storm
1.0.0-beta4indexedHigh-performance lightweight SQL ORM with strongly-typed SQL DSL surfacing compile-time errors, automated entity mapping, bundled SQLite drivers, expressive querying supporting joins, aggregates, transactions.
High-performance lightweight SQL ORM with strongly-typed SQL DSL surfacing compile-time errors, automated entity mapping, bundled SQLite drivers, expressive querying supporting joins, aggregates, transactions.
Storm 是一个直接基于纯 Kotlin 编写的高效简洁的轻量级 Kotlin Multiplatform 与 Android SQL ORM
框架,提供了强类型的 SQL DSL,直接将低级 bug 暴露在编译期。
英文版请参见 README_EN.md。
dependencies {
implementation("com.zxhhyj.storm:storm-core:<version>")
implementation("com.zxhhyj.storm:storm-driver-androidx-sqlite:<version>")
// 这里以 androidx-sqlite-bundled 为例
implementation("androidx.sqlite:sqlite-bundled:<version>")
// implementation("androidx.sqlite:sqlite-framework:<version>")
}
以下是支持的驱动列表:
| 模块 | 适用平台 |
|---|
UserInfo 表import com.zxhhyj.storm.schema.Table
import com.zxhhyj.storm.schema.int
import com.zxhhyj.storm.schema.varchar
object UserInfoTable : Table<Nothing>("t_user_info") {
val id = int("id").primaryKey().autoincrement()
val name = varchar("name").notNull()
val info = varchar("info")
}
CREATE TABLE IF NOT EXISTS "t_user_info" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"info" TEXT
);
Table<Nothing> 表示这是一个纯行级 schema。id 列生成 INTEGER PRIMARY KEY AUTOINCREMENT,其余列按
notNull() 决定是否可空。varchar("name") 只接收列名——没有 length 参数。
User 表import com.zxhhyj.storm.schema.ForeignKeyAction
import com.zxhhyj.storm.schema.Table
import com.zxhhyj.storm.schema.datetime
import com.zxhhyj.storm.schema.int
import com.zxhhyj.storm.schema.varchar
object UserTable : Table<Nothing>("t_user") {
val id = int("id").primaryKey().autoincrement()
val email = varchar().notNull()
userInfoId =
int().references(UserInfoTable, onDelete = ForeignKeyAction.CASCADE)
createDateTime = datetime().notNull()
}
CREATE TABLE IF NOT EXISTS "t_user" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"user_info_id" INTEGER REFERENCES "t_user_info"("id") ON DELETE CASCADE,
"create_date_time" TEXT NOT NULL
);
references(otherTable, onDelete = …) 会生成 REFERENCES t_user_info(id) ON DELETE CASCADE。
Table<*>,不是单个 Column —— 默认指向目标表的 id 列。ReadWriteDatabase 通过工厂函数创建,提供参数式与 DSL 式两种风格。把
onDatabaseCreate / onCreate / onClose 作为参数传入即可声明表结构与生命周期逻辑。
参数式:
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import com.zxhhyj.storm.database.ReadWriteDatabase
import com.zxhhyj.storm.database.createTableIfNotExists
import com.zxhhyj.storm.AndroidXSQLiteConnection
import com.zxhhyj.storm.crud.insert
val appDatabase = ReadWriteDatabase(
connection = AndroidXSQLiteConnection(BundledSQLiteDriver().open("app.db")),
version = 1,
onDatabaseCreate = {
createTableIfNotExists(UserInfoTable)
createTableIfNotExists(UserTable)
},
onCreate = {},
onClose = {},
)
DSL 式(等价写法):
val appDatabase = ReadWriteDatabase {
connection = AndroidXSQLiteConnection(BundledSQLiteDriver().open("app.db"))
version = 1
onDatabaseCreate {
createTableIfNotExists(UserInfoTable)
createTableIfNotExists(UserTable)
}
onCreate {}
onClose {}
}
PRAGMA foreign_keys = ;
IF "storm_meta" ("key" TEXT , "value" );
IF "t_user_info" ("id" AUTOINCREMENT, "name" TEXT , "info" TEXT);
IF "t_user" ("id" AUTOINCREMENT, "email" TEXT , "user_info_id" "t_user_info"("id") CASCADE, "create_date_time" TEXT );
onDatabaseCreate 在 onCreate 之前运行(同一个事务里,但单独追踪); onCreate 每次 init 结束都跑;
onClose 在调用 close() 时执行。
只读场景(报表、查询服务等)用 ReadOnlyDatabase——insert / update / delete / execSQL /
transaction 等写 API 只挂在 ReadWriteDatabase 上,在 ReadOnlyDatabase 上调用直接编译不通过。
import com.zxhhyj.storm.database.ReadOnlyDatabase
import com.zxhhyj.storm.crud.form
import com.zxhhyj.storm.crud.querySingle
val readOnly = ReadOnlyDatabase(
AndroidXSQLiteConnection(BundledSQLiteDriver().open("app.db"))
)
val count = readOnly.form(UserInfoTable).selectAll().count()
val alice = readOnly.querySingle(UserInfoTable) { UserInfoTable.name eq "Hao" }!!
// readOnly.insert(...) // 编译期就不行
// readOnly.execSQL("DELETE …") // 编译期就不行
CREATE TABLE "t_article" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"rating" REAL,
"published" INTEGER NOT NULL DEFAULT 0,
"created_at" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
"birth" TEXT,
"status" INTEGER ,
"word_count"
);
所有修饰符都可链式调用,按书写顺序生效:
ForeignKeyAction 可选值:NO_ACTION、RESTRICT、SET_NULL、SET_DEFAULT、CASCADE; 可选值:
、、。
为简洁起见,下文所有 suspend 函数示例均假设运行在协程作用域内,不再包裹 runBlocking。
import com.zxhhyj.storm.crud.insert
appDatabase.transaction {
insert(UserInfoTable) {
this[UserInfoTable.name] = "Hao"
this[UserInfoTable.info] = "keep going"
}
insert(UserInfoTable) {
this[UserInfoTable.name] = "Ada"
this[UserInfoTable.info] = null
}
}
INSERT INTO "t_user_info" ("name", "info") VALUES (?, ?);
INSERT INTO "t_user_info" ("name", "info") VALUES (?, NULL);
import com.zxhhyj.storm.crud.batchInsert
appDatabase.transaction {
batchInsert(
UserInfoTable, listOf(
mapOf("name" to "Lin", "info" to "x"),
mapOf("name" to "Grace", "info" to "y"),
)
) { row ->
this[UserInfoTable.name] = row["name"] as String
this[UserInfoTable.info] = row["info"] as String?
}
}
INSERT INTO "t_user_info" ("name", "info") VALUES (?, ?), (?, ?);
import com.zxhhyj.storm.crud.upsert
appDatabase.upsert(UserInfoTable).set {
this[UserInfoTable.name] = "Dennis"
this[UserInfoTable.info] = "C"
}.onConflict(UserInfoTable.email).execute()
INSERT INTO "t_user_info" ("name", "info") VALUES (?, ?)
ON CONFLICT ("email") DO UPDATE SET "name" = excluded."name", "info" = excluded."info";
当表上有
references列时,被引用行要先插入,且与引用行同事务。storm-core提供的batchInsert只接Map;实体级批量插入见 13.1。
import com.zxhhyj.storm.crud.update
appDatabase.transaction {
update(UserInfoTable) {
this[UserInfoTable.name] = "updated"
} where { UserInfoTable.name eq "Hao" }
}
UPDATE "t_user_info" SET "name" = ? WHERE "name" = ?;
import com.zxhhyj.storm.crud.updateAll
appDatabase.updateAll(UserInfoTable) { this[UserInfoTable.info] = "bulk" }
UPDATE "t_user_info" SET "info" = ?;
import com.zxhhyj.storm.crud.deleteWhere
appDatabase.deleteWhere(UserInfoTable) { UserInfoTable.name eq "Hao" }
DELETE FROM "t_user_info" WHERE "name" = ?;
import com.zxhhyj.storm.crud.deleteAll
appDatabase.transaction { deleteAll(UserInfoTable) }
DELETE FROM "t_user_info";
update / deleteWhere 是 suspend 扩展;在 transaction { … } 内可使用对应的事务内版本(非
suspend)。
import com.zxhhyj.storm.crud.querySingle
val user: QueryBuilder.ResultSet? =
appDatabase.querySingle(UserInfoTable) { UserInfoTable.name eq "Hao" }
SELECT * FROM "t_user_info" WHERE "name" = ? LIMIT 1;
import com.zxhhyj.storm.crud.form
val users: List<QueryBuilder.ResultSet> = appDatabase.form(UserInfoTable).selectAll().toList()
SELECT * FROM "t_user_info";
val count: Long = appDatabase.form(UserInfoTable).selectAll().count()
SELECT COUNT(*) FROM "t_user_info";
form(table) 返回 FormBuilder(尚未生成 SELECT);selectAll() 把它转成 QueryBuilder 才能跑。
ColumnRef 的运算符也接受其他 ColumnRef —— 在 JOIN 谓词里很有用:
where { ArticleTable.authorId eq UserTable.id }
FormBuilder / QueryBuilderform(table) 返回 FormBuilder。调用 selectAll() 或 select(…) 才进入 QueryBuilder,之后链式挂
WHERE / GROUP / HAVING / ORDER / LIMIT。
import com.zxhhyj.storm.dsl.SortOrder
val top10: List<QueryBuilder.ResultSet> = appDatabase.form(UserTable)
.select(UserTable.id, UserTable.email)
.innerJoin(UserInfoTable) { UserTable.userInfoId eq UserInfoTable.id }
.where { UserTable.email like "%@example.com%" }
.groupBy(UserInfoTable.id)
.having { UserInfoTable.id gte 1L }
.orderBy(UserTable.id, SortOrder.DESC)
.limit(10)
.offset(20)
.toList()
SELECT "id", "email"
FROM "t_user"
INNER JOIN "t_user_info" ON "t_user"."user_info_id" = "t_user_info"."id"
WHERE "t_user"."email" LIKE ?
GROUP BY "t_user_info"."id"
HAVING "t_user_info"."id" >= ?
ORDER BY "t_user"."id"
LIMIT
;
where { … }、having { … }、set { … }(在 UpdateBuilder / UpsertBuilder 里)都是**单次调用契约
**——重复调用会抛
IllegalStateException。多个条件用 and / or 在一个 block 里表达。
支持的 JOIN:innerJoin / leftJoin / rightJoin(都接谓词),crossJoin(不接谓词)。
聚合:
form(UserTable) {
select(
countColumn(),
sum(UserTable.age),
avg(UserTable.age),
min(UserTable.age),
max(UserTable.age),
groupConcat(UserTable.name, ", "),
)
}
DDL 扩展函数以 ReadWriteDatabase 为 receiver(因为它们内部调 execSQL),所以只能在读写句柄上使用:
import com.zxhhyj.storm.database.createTableIfNotExists
appDatabase.createTableIfNotExists(UserTable)
CREATE TABLE IF NOT EXISTS "t_user" (...);
import com.zxhhyj.storm.database.dropTableIfExists
appDatabase.dropTableIfExists(UserTable)
DROP TABLE IF EXISTS "t_user";
import com.zxhhyj.storm.database.dropIndex
appDatabase.dropIndex("idx_user_email", ifExists = true)
DROP INDEX IF EXISTS "idx_user_email";
import com.zxhhyj.storm.database.createTableAndIndex
import com.zxhhyj.storm.schema.ddl.IfNotExists
val statements: List<String> = UserTable createTableAndIndex IfNotExists
statements.forEach { appDatabase.execSQL(it) }
CREATE TABLE IF NOT EXISTS "t_user" (...);
val createSql: String = UserTable.createTable(ifNotExists = true)
val dropSql: String = UserTable.dropTable(ifExists = true)
val addColumnSql: String = UserTable addColumn UserTable.password
CREATE TABLE IF NOT EXISTS "t_user" (...);
DROP TABLE IF EXISTS "t_user";
ALTER TABLE "t_user" ADD COLUMN ...;
transaction { … } 内的 receiver 是 TransactionScope,不是 Database——DDL
必须在事务外执行(或者在 migrations 迁移步骤里执行,见 11)。
Storm 通过 InvalidationTracker 追踪每个被触碰的 Table。读操作返回 Flow
,当任意被观察的表在已提交事务内发生变化时重新发射。
import com.zxhhyj.storm.observe.observe
val count: Flow<Long> = appDatabase.observe(UserTable) { ops ->
ops.form(UserTable).selectAll().count()
}
val filtered: Flow<List<QueryBuilder.ResultSet>> =
appDatabase.form(UserTable).selectAll()
.where { UserTable.age gte 18 }
.observe()
filtered.collect { rows -> println("got ${rows.size} adults") }
Flow 才会重新发射。(connection, table-set) 上的多次订阅会被自动去重。import com.zxhhyj.storm.crud.insert
appDatabase.transaction {
insert(UserInfoTable) {
this[UserInfoTable.name] = "Hao"
this[UserInfoTable.info] = "x"
}
}
appDatabase.transaction {
appDatabase.transaction {
println(depth)
println(savepointName)
}
}
transaction { … } 是 suspend。迁移通过 migrations 参数声明——传入一个 MigrationPlan.() -> Unit lambda,用 migrate(from, to) { … }
注册每一步。每个 block 拿到 ReadWriteDatabase 作为 receiver,所以可以直接调 execSQL。
给 UserTable 加一个 password 列,版本号从 1 升到 2:
ALTER TABLE "t_user" ADD COLUMN password VARCHAR NOT NULL DEFAULT '';
migrate(from, to) 按 from 顺序执行;每个 block 自己包一个 SAVEPOINT,单个失败不会破坏整次升级。
appDatabase.enableCaseSensitiveLike()
appDatabase.disableCaseSensitiveLike()
println(appDatabase.foreignKeysEnabled)
@OptIn(ExperimentalStormApi::class)
appDatabase.execSQL("VACUUM")
storm-orm(可选)用 data class 直接对应 Table<T>,读写表行不再写 SQL。API 暂标 @ExperimentalStormOrmApi。
INSERT INTO "t_user_info" ("id", "name", "info") VALUES (?, ?, ?);
@OptIn(ExperimentalStormOrmApi::class)
appDatabase.upsert(UserInfoTable, UserInfo(id = 1L, name = "Hao", info = "y"))
INSERT INTO "t_user_info" ("id", "name", "info") VALUES (?, ?, ?)
ON CONFLICT ("id") DO UPDATE SET "name" = excluded."name", "info" = excluded."info";
@OptIn(ExperimentalStormOrmApi::class)
appDatabase.upsert(UserInfoTable, UserInfo(id = 2L, name = "Ada", info = null), UserInfoTable.name)
INSERT INTO "t_user_info" ("id", "name", "info") VALUES (?, ?, NULL)
ON CONFLICT ("name") DO UPDATE SET "id" = excluded."id", "info" = excluded."info";
appDatabase.insert(table, entity) 和 appDatabase.upsert(table, entity) 都是 suspend。
@OptIn(ExperimentalStormOrmApi::class)
val all: List<UserInfo> = appDatabase.entities(UserInfoTable).toList()
SELECT * FROM "t_user_info";
@OptIn(ExperimentalStormOrmApi::class)
val filtered: List<UserInfo> = appDatabase.entities(UserInfoTable)
.where { UserInfoTable.name eq "Hao" }
.orderBy(UserInfoTable.id, com.zxhhyj.storm.dsl.SortOrder.DESC)
.limit(10)
.toList()
SELECT * FROM "t_user_info" WHERE "name" = ? ORDER BY "id" DESC LIMIT 10;
@OptIn(ExperimentalStormOrmApi::class)
val first: UserInfo? = appDatabase.entities(UserInfoTable)
.where { UserInfoTable.name eq "Hao" }
.firstOrNull()
SELECT * FROM "t_user_info" WHERE "name" = ? LIMIT 1;
appDatabase.entities(table).toList() 和 firstOrNull() 都是 suspend;EntityBuilder 还提供
count /
/ / / / / 。
@OptIn(ExperimentalStormOrmApi::class)
val flow: Flow<List<UserInfo>> = appDatabase
.entities(UserInfoTable)
.observe { it.where { UserInfoTable.name eq "Hao" }.toList() }
欢迎提 Issues 和 PR,你的 Issues 就是我最大的动力,你的 PR 是对我最大的支持。
本项目基于 Apache License 2.0。
如有疑问或建议,请提 Issues 或者写一封邮件发送到 zxhhyj@qq.com。
storm-driver-androidx-sqlite | KMP 与 Android(androidx.sqlite,可选 sqlite-bundled 或 sqlite-framework) |
storm-driver-android-sqlite | Android(直接封装 android.database.sqlite) |
import com.zxhhyj.storm.schema.Table
import com.zxhhyj.storm.schema.boolean
import com.zxhhyj.storm.schema.date
import com.zxhhyj.storm.schema.datetime
import com.zxhhyj.storm.schema.enum
import com.zxhhyj.storm.schema.int
import com.zxhhyj.storm.schema.long
import com.zxhhyj.storm.schema.real
import com.zxhhyj.storm.schema.varchar
import com.zxhhyj.storm.schema.check
object ArticleTable : Table<Nothing>("t_article") {
val id = int("id").primaryKey().autoincrement()
val title = varchar("title").notNull()
val rating = real("rating").nullable()
val published = boolean("published").notNull().default(false)
val createdAt = datetime("created_at").notNull().defaultCurrentTimestamp()
val birth = date("birth").nullable()
val status = enum<Status>("status").notNull()
val wordCount = long("word_count").notNull().default(0L)
}
| 工厂 | SQLite 存储 | Kotlin 类型 |
|---|
int | INTEGER | Int |
long | INTEGER | Long |
real | REAL | Float |
varchar | VARCHAR | String |
boolean | INTEGER (0/1) | Boolean |
date | VARCHAR (ISO date) | LocalDate |
datetime | VARCHAR (ISO dt) | LocalDateTime |
enum<E> | INTEGER (ordinal) | E |
val id = int("id").primaryKey().autoincrement()
val email = varchar("email").notNull().unique()
val nickname = varchar("nick").nullable()
val parentId = int("parent_id").references(CategoryTable, onDelete = ForeignKeyAction.SET_NULL)
val archived = boolean("archived").notNull().default(false)
val score = real("score").nullable().default(0f)
val refreshedAt = datetime("refreshed_at").defaultCurrentTimestamp()
val birthTime = date("birth_time").defaultCurrentDate()
val locale = varchar("locale").collate(Collation.NOCASE)
val token = varchar("token").check { it neq "" }
val tags = varchar("tags").transform(
save = { list: List<String> -> list.joinToString(",") },
restore = { csv: String -> csv.split(",").filter(String::isNotEmpty) },
)
CollationBINARYNOCASERTRIMimport com.zxhhyj.storm.dsl.ConditionalExpression.Companion.between
import com.zxhhyj.storm.dsl.ConditionalExpression.Companion.eq
import com.zxhhyj.storm.dsl.ConditionalExpression.Companion.eqDistinct
import com.zxhhyj.storm.dsl.ConditionalExpression.Companion.gte
import com.zxhhyj.storm.dsl.ConditionalExpression.Companion.isNotNull
import com.zxhhyj.storm.dsl.ConditionalExpression.Companion.isNull
import com.zxhhyj.storm.dsl.ConditionalExpression.Companion.like
import com.zxhhyj.storm.dsl.ConditionalExpression.Companion.lt
import com.zxhhyj.storm.dsl.ConditionalExpression.Companion.neqDistinct
import com.zxhhyj.storm.dsl.ConditionalExpression.Companion.notBetween
where { UserInfoTable.name eq "Hao" }
where { UserInfoTable.age gte 18 }
where { UserInfoTable.age lt 30 }
where { UserInfoTable.name like "%a%" }
where { UserInfoTable.info.isNull() }
where { UserInfoTable.info.isNotNull() }
where { UserInfoTable.age between (18..65) }
where { UserInfoTable.age notBetween (18..65) }
where { UserInfoTable.name eqDistinct null }
where { UserInfoTable.name neqDistinct null }
where { (UserInfoTable.age gte 18) and (UserInfoTable.name like "%a%") }
| 运算符 | SQL 渲染 |
|---|
a eq b / a == b | a = b |
a neq b / a != b | a <> b |
a gt b / a > b | a > b |
a gte b / a >= b | a >= b |
a lt b / a < b | a < b |
a lte b / a <= b | a <= b |
a like pattern | a LIKE pattern |
a.between(range) / notBetween(r) | a BETWEEN x AND y |
a.in(list) / notIn(list) | a IN (?, ?, …) |
a.in(qb) | a IN (SELECT …) (infix) |
a.isNull() / isNotNull() | a IS [NOT] NULL |
a eqDistinct b / neqDistinct b | a IS b / IS NOT b |
cond and cond / or / not(cond) | AND / OR / NOT |
TransactionScopeinsertbatchInsertupdatedeleteAlldirty(table)upsert(table, entity)import com.zxhhyj.storm.ExperimentalStormApi
import com.zxhhyj.storm.database.ReadWriteDatabase
import com.zxhhyj.storm.crud.insert
object UserTable : Table<Nothing>("t_user") {
val id = int("id").primaryKey().autoincrement()
val email = varchar("email").notNull()
val password = varchar("password").notNull()
val userInfoId =
int("user_info_id").references(UserInfoTable, onDelete = ForeignKeyAction.CASCADE)
val createDateTime = datetime("create_date_time").notNull()
}
val appDatabase = ReadWriteDatabase(
connection = AndroidXSQLiteConnection(BundledSQLiteDriver().open("app.db")),
version = 2,
migrations = {
migrate(1, 2) {
execSQL("ALTER TABLE t_user ADD COLUMN password VARCHAR NOT NULL DEFAULT ''")
}
},
onDatabaseCreate = {
createTableIfNotExists(UserTable)
},
onCreate = {},
onClose = {},
)
import com.zxhhyj.storm.orm.insert
import com.zxhhyj.storm.orm.upsert
import com.zxhhyj.storm.orm.entities
import com.zxhhyj.storm.orm.ExperimentalStormOrmApi
import com.zxhhyj.storm.schema.Table
import com.zxhhyj.storm.schema.int
import com.zxhhyj.storm.schema.varchar
data class UserInfo(
val id: Long = 0L,
val name: String,
val info: String?,
)
object UserInfoTable : Table<UserInfo>("t_user_info") {
val id = int("id").primaryKey().autoincrement().bindTo(UserInfo::id)
val name = varchar("name").notNull().bindTo(UserInfo::name)
val info = varchar("info").bindTo(UserInfo::info)
}
appDatabase.insert(UserInfoTable, UserInfo(name = "Hao", info = "x"))
orderBygroupByhavinglimitoffsetobserve { … }Surfaced from shared tags and platforms — no rankings paid for.