Storm 是直接基于纯Kotlin编写的高效简洁的轻量级Kotlin Multiplatform SQL ORM框架,提供了强类型的SQL DSL,直接将低级bug暴露在编译期。
在libs.versions.toml中定义版本:
[versions]
storm = "1.0.0-alpha8"
[libraries]
storm-core = { module = "com.zxhhyj:storm-core", version.ref = "storm" }
storm-orm = { module = "com.zxhhyj:storm-orm", version.ref = "storm" }
# 内置 SQLite 驱动(KMP,推荐,含 iOS/JVM/Desktop)
storm-driver-androidx-bundled = { module = "com.zxhhyj:storm-driver-androidx-sqlite-bundled", version.ref = "storm" }
# 框架 SQLite 驱动(KMP,Android/iOS/macOS/Linux)
storm-driver-androidx-framework = { module = "com.zxhhyj:storm-driver-androidx-sqlite-framework", version.ref = "storm" }
[plugins]
storm = { id = "com.zxhhyj.storm", version.ref = "storm" }在build.gradle.kts中添加依赖(按需二选一):
plugins { alias(libs.plugins.storm) }
commonMain.dependencies {
implementation(libs.storm.core) // 必选
implementation(libs.storm.orm) // 需要实体自动映射时加
}
// 内置 SQLite 驱动(KMP,推荐,跨平台一致性最强)
implementation(libs.storm.driver.androidx.bundled)
// 或:框架 SQLite 驱动(使用平台提供的 SQLite)
implementation(libs.storm.driver.androidx.framework)
// 需要 entities() 自动实体映射时才加:
dependencies {
add("kspJvm", projects.stormKsp)
add("kspAndroid", projects.stormKsp)
}驱动区别详见 驱动 章节。
这里以用户表和用户信息表为示例:
创建User.kt文件,并实现以下代码:
data class User(
val id: Long = 0,
val name: String,
val info: String,
)
object UserTable : Table<User>("t_user") {
val id = long("id").primaryKey(autoincrement = true).bindTo(User::id)
val name = varchar("name").bindTo(User::name)
val info = varchar("info").bindTo(User::info)
}其中User为实体模型,UserTable为表的Schema,在其中使用列类型API来定义列。
创建UserInfo.kt文件,并使用referencesAPI建立表关联:
data class UserInfo(
val id: Long = 0,
val email: String,
val userId: Long = 0,
)
object UserInfoTable : Table<UserInfo>("t_user_info") {
val id = long("id").primaryKey(autoincrement = true).bindTo(UserInfo::id)
val email = varchar("email").bindTo(UserInfo::email)
val userId = long("user_id").references(UserTable).bindTo(UserInfo::userId)
}Storm 提供了两种 SQLite 驱动,按需选择其一:
方式一:内置 SQLite 驱动(Bundled)(KMP,推荐,跨平台一致性最强)
使用 androidx.sqlite:sqlite-bundled,自带编译好的 SQLite 库,支持 Android / JVM / iOS / macOS / Linux。
val connection = BundledSQLiteConnection(BundledSQLiteDriver().open("app.db"))
val database = SQLiteDatabase.connect(connection)
database.execSQL(UserTable createTable IfNotExists)
database.execSQL(UserInfoTable createTable IfNotExists)方式二:框架 SQLite 驱动(Framework)(使用平台提供的 SQLite)
使用 androidx.sqlite:sqlite-framework,委托给各平台自带的 SQLite 实现(Android / iOS / macOS / Linux),不含 JVM。
val connection = AndroidXSQLiteConnection(androidx.sqlite.driver.AndroidSQLiteDriver().open("app.db"))
val database = SQLiteDatabase.connect(connection)
database.execSQL(UserTable createTable IfNotExists)
database.execSQL(UserInfoTable createTable IfNotExists)SQLiteDatabase提供了insert、update、delete和form/entities等API。
// 实体插入(返回自增 ID)
val rowId = database.insert(UserTable, User(name = "张三", info = "无限进步"))
database.insert(UserInfoTable, UserInfo(email = "zxhhyj@qq.com", userId = rowId))
// DSL 插入
database.insert(UserTable) {
this[UserTable.name] = "李四"
this[UserTable.info] = "无限进步"
}
// 批量插入
database.batchInsert(UserTable, listOf(
User(name = "张三", info = "无限进步"),
User(name = "李四", info = "好好学习"),
))
// UPSERT — 主键冲突时自动更新
database.upsert(UserTable, User(id = 1, name = "王五", info = "覆盖更新"))// 删除全部
database.deleteAll(UserTable)
// 按条件删除
database.delete(UserTable).where { UserTable.id eq 1 }.execute()
// 按条件删除(快捷方式)
database.deleteWhere(UserTable) { UserTable.id eq 1 }
// querySingle — 单行查询快捷方法
val result: QueryBuilder.ResultSet? = database.querySingle(UserTable) { UserTable.name eq "张三" }database.update(UserTable)
.set { this[UserTable.name] = "new name" }
.where { UserTable.id eq 1 }
.execute()
// 更新全部
database.updateAll(UserTable) { this[UserTable.info] = "无限进步" }需要 KSP 编译期代码生成。
// 查询多个
val users = database.entities(UserTable)
.where { UserTable.name eq "张三" }
.orderBy(UserTable.id, SortOrder.DESC)
.toList()
// 查询单个
val user = database.entities(UserTable)
.where { UserTable.id eq 1 }
.firstOrNull()适用于部分列查询和 JOIN,不需要 KSP。
// 基础查询
val rows = database.form(UserTable)
.select(UserTable.name, UserTable.info)
.where { UserTable.age gte 18 }
.limit(10)
.toList()
rows.forEach { row ->
println("${row[UserTable.name]} ${row[UserTable.info]}")
}
// SELECT DISTINCT
val distinctNames = database.form(UserTable)
.select(UserTable.name, distinct = true)
.toList()
// GROUP BY + HAVING
val groups = database.form(UserTable)
.select(UserTable.name, UserTable.age)
.groupBy(UserTable.name)
.having { UserTable.age gte 18 }
.toList()
// 聚合函数
database.form(UserTable).selectAll().count() // 总行数
database.form(UserTable).selectAll().countColumn(UserTable.name) // 非空列计数
database.form(UserTable).selectAll().minBoolean(UserTable.isActive) // 布尔聚合
database.form(UserTable).selectAll().maxBoolean(UserTable.isActive)
// QueryIterator — 手动迭代,支持 use{} 提前安全关闭
database.form(UserTable).selectAll().iterator().use { iter ->
for (row in iter) {
if (someCondition) break // 安全退出,迭代器自动关闭底层 Statement
}
}database.form(UserTable)
.select(UserTable.name, UserInfoTable.email)
.innerJoin(UserInfoTable) { UserTable.id eq UserInfoTable.userId }
.where { UserTable.name eq "张三" }
.toList()
.forEach { row ->
println("${row[UserTable.name]} ${row[UserInfoTable.email]}")
}
// LEFT JOIN · RIGHT JOIN · CROSS JOIN
database.form(UserTable).leftJoin(UserInfoTable) { UserTable.id eq UserInfoTable.userId }
database.form(UserTable).rightJoin(UserInfoTable) { UserTable.id eq UserInfoTable.userId }
database.form(UserTable).crossJoin(RoleTable)database.entities(UserTable).count() // → Long
database.entities(UserTable).countColumn(UserTable.name) // → Long(非空计数)
database.entities(UserTable).sum(UserTable.age) // → Double
database.entities(UserTable).avg(UserTable.age) // → Double
database.entities(UserTable).min(UserTable.age) // → Double?
database.entities(UserTable).max(UserTable.age) // → Double?
// LIMIT/OFFSET 存在时,聚合自动在子集上计算
database.form(UserTable).selectAll().limit(5).count() // 前 5 行的计数// 挂起事务(推荐,自动追踪脏表)
database.transaction {
insert(UserTable, User(name = "张三"))
insert(UserInfoTable, UserInfo(email = "zxhhyj@qq.com"))
}
// 提交后自动通知 observe 观察者
// 阻塞事务(非协程环境)
database.blockingTransaction {
database.execSQL("INSERT ...")
}当数据变更时自动重新查询,无需手动轮询。支持两种方式:
import com.zxhhyj.storm.observe.observe基于表的 observe — 监听指定表的数据变更:
db.observe(UserTable) { ops ->
ops.form(UserTable).selectAll().count()
}.collect { count ->
println("用户数量: $count") // 首次订阅立即输出→后续每次变更自动输出
}基于 QueryBuilder 的 observe — 自动推导 JOIN 中所有关联表:
db.form(UserTable)
.innerJoin(UserInfoTable) { UserTable.id eq UserInfoTable.userId }
.observe { q -> q.toList() }
.collect { rows ->
println("查询结果: $rows")
}基于 EntityBuilder 的 observe — ORM 层一体的响应式查询:
db.entities(UserTable)
.where { UserTable.status eq "active" }
.observe { it.toList() }
.collect { users ->
println("活跃用户: $users")
}特性:
- 事务失败回滚时不会触发通知
- 同一个事务中的多次写入会合并为一次通知
- 写无关表时不会触发通知
- 无 observer 注册时零开销
// observe 完整示例
val job = launch {
db.observe(UserTable) { ops ->
ops.form(UserTable).selectAll().count()
}.collect { count ->
println("用户数量: $count")
}
}
db.transaction {
insert(UserTable, User(name = "张三"))
insert(UserTable, User(name = "李四"))
}
// collect 输出: 0(初始)→ 2(事务提交后,两次插入合并为一次)创建AppDatabase.kt文件,继承Database:
class AppDatabase(connection: SQLiteConnection) : Database(connection, version = 3) {
override fun onCreate() {
execSQL(UserTable.createTable())
}
override fun onConstruct() {
migrate(1 to 2) { execSQL(UserTable addColumn UserTable.email) }
migrate(2 to 3) { execSQL(UserTable addColumn UserTable.info) }
}
}
val db = AppDatabase(connection)
// db.migrate() 在 onConstruct() 中注册后自动执行,无需手动调用或 DSL 方式:
val db = database(connection) {
version = 3
create { execSQL(UserTable.createTable()) }
migrate(1 to 2) { execSQL(UserTable addColumn UserTable.email) }
migrate(2 to 3) { execSQL(UserTable addColumn UserTable.info) }
}
onConstruct()是构造器自动调用的钩子,在其中注册的迁移在构造函数返回前自动执行。迁移链断裂时会在构造时抛异常,确保 schema 一致性。
参考连接数据库,创建不同的SQLiteConnection即可。
使用中缀 DSL 风格快速生成 SQL 语句:
UserTable createTable IfNotExists // CREATE TABLE IF NOT EXISTS
UserTable dropTable IfExists // DROP TABLE IF EXISTS
"idx_t_user_name" dropIndex IfExists // DROP INDEX IF EXISTS
UserTable addColumn UserTable.email // ALTER TABLE ADD COLUMN
UserTable createTableAndIndex IfNotExists // 建表 + UNIQUE 列索引(返回 List<String>)
// 非中缀风格
UserTable.createTable() // CREATE TABLE(不带 IF NOT EXISTS)
UserTable.createTable(ifNotExists = true) // CREATE TABLE IF NOT EXISTS
UserTable.dropTable(ifExists = true) // DROP TABLE IF EXISTS
"idx_t_user_name".dropIndex(ifExists = true) // DROP INDEX IF EXISTS| 模块 | 内容 | 类别 |
|---|---|---|
storm-core |
Database、SQLiteConnection、Table、Column、CRUD、迁移、observe 响应式查询 |
非实体 |
storm-orm |
entities()、EntityBuilder、packing()、StormTablePackerRegistry |
ORM 实体映射 |
storm-driver-androidx-sqlite-bundled |
BundledSQLiteConnection(Android / JVM / iOS / macOS / Linux) |
驱动 |
storm-driver-androidx-sqlite-framework |
AndroidXSQLiteConnection(Android / iOS / macOS / Linux) |
驱动 |
storm-ksp |
StormTablePackerProcessor |
KSP 代码生成 |
storm-gradle-plugin |
com.zxhhyj.storm |
Gradle 插件 |
// 场景 1:纯 SQL DSL / Query Builder,不碰实体
implementation("com.zxhhyj:storm-core:1.0.0-alpha8")
// 场景 2:需要类型安全实体映射
plugins { id("com.zxhhyj.storm") version "1.0.0-alpha8" }
implementation("com.zxhhyj:storm-orm:1.0.0-alpha8")
// 场景 3a:内置 SQLite 驱动(KMP,推荐)
implementation("com.zxhhyj:storm-driver-androidx-sqlite-bundled:1.0.0-alpha8")
// 场景 3b:框架 SQLite 驱动(Android / iOS / macOS / Linux)
implementation("com.zxhhyj:storm-driver-androidx-sqlite-framework:1.0.0-alpha8")Storm 把底层 SQLiteConnection 抽象为可插拔驱动,目前内置两个实现,二者都基于 androidx.sqlite 官方库:
模块:com.zxhhyj:storm-driver-androidx-sqlite-bundled
封装 androidx.sqlite:sqlite-bundled,底层为从源码编译的内置 SQLite 库,各平台版本完全一致。
特点:
- 支持
Kotlin Multiplatform:Android / JVM / iOS / macOS / Linux - 不依赖 Android Framework,不传
Context - 所有平台使用同一版本 SQLite,行为完全一致
- blob / 大字段无任何限制
使用:
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import com.zxhhyj.storm.BundledSQLiteConnection
val connection = BundledSQLiteConnection(BundledSQLiteDriver().open("app.db"))
val database = SQLiteDatabase.connect(connection)模块:com.zxhhyj:storm-driver-androidx-sqlite-framework
封装 androidx.sqlite:sqlite-framework,委托给各平台自带的 SQLite 实现(Android 的 android.database.sqlite、Apple 的 Core Data、Linux 的系统 SQLite 库)。
特点:
- 支持平台:Android / iOS / macOS / Linux
- 使用平台自带的 SQLite 库,编译产物更小
- 行为与平台 SQLite 版本相关
使用:
import androidx.sqlite.driver.AndroidSQLiteDriver
import com.zxhhyj.storm.AndroidXSQLiteConnection
val connection = AndroidXSQLiteConnection(AndroidSQLiteDriver().open("app.db"))
val database = SQLiteDatabase.connect(connection)| 场景 | 推荐驱动 |
|---|---|
| KMP 项目需要跨平台一致性(JVM / Android / iOS / 桌面) | storm-driver-androidx-sqlite-bundled |
| Android 项目需要复用系统 SQLite,减少包体积 | storm-driver-androidx-sqlite-bundled 或 storm-driver-androidx-sqlite-framework |
| iOS / macOS / Linux 平台项目,使用平台 SQLite | storm-driver-androidx-sqlite-framework |
| 需要在查询路径中绑定 Blob | 两者均支持 |
| JVM Desktop / Server | storm-driver-androidx-sqlite-bundled |
下面整理了一些 Storm 提供的实用能力,覆盖从 Schema 定义到查询执行的多个环节,日常开发中可能会用到。
如果不想定义实体类,或者字段需要做复杂转换(比如密封类/多态),可以直接用 RowBuilder 手动构建行数据:
// 手动构建行,插入任意数据
val row = RowBuilder().apply {
this[UserTable.name] = "张三"
this[UserTable.info] = "无限进步"
}.build()
db.execSQL(UserTable insert row)
// 配合 sealed class 字段映射
val row2 = RowBuilder().apply {
this[TriggerTable.name] = "Morning"
this[TriggerTable.enabled] = "true"
when (event) {
is TriggerEvent.Time -> {
this[TriggerEventTable.type] = "time"
this[TriggerEventTable.remindAt] = event.remindAt
}
is TriggerEvent.Event -> {
this[TriggerEventTable.type] = "event"
this[TriggerEventTable.eventType] = event.eventType
}
}
}.build()
db.execSQL(TriggerEventTable insert row2)将任意 Kotlin 类型映射到 SQLite 存储类型:
object UserTable : Table<User>("t_user") {
// Boolean → 0/1(内置)
val isActive = boolean("is_active")
// 自定义 UUID → String 映射
val uuid = varchar("uuid").transform(
save = { it: UUID -> it.toString() },
restore = { it: String -> UUID.fromString(it) },
)
// 使用预构建的 Transform 对象
val status = varchar("status").transform(
Column.Transform<UserStatus, String>(
save = { it.name },
restore = { UserStatus.valueOf(it) },
)
)
}内置转换器:BooleanTypeConverters、DateTypeConverters、TimestampTypeConverters。
Storm 还预置了便捷列类型:
boolean(name)— 存储为Long(0/1)date(name)/datetime(name)— 存储为 ISO-8601Stringenum(name)— 存储为枚举name字符串
在列定义时直接编写 CHECK 约束,支持组合逻辑:
object ProductTable : Table<Product>("t_product") {
val price = real("price").check {
(gte 0) and (lt 1000000)
}
val age = int("age").check {
(gte 0) and (lte 150)
}
val status = varchar("status").check {
(eq "active") or (eq "inactive") or (eq "archived")
}
}生成的 DDL:CREATE TABLE t_product (price REAL CHECK(price >= 0 AND price < 1000000), ...)
references() 有两个重载,完整版支持 onDelete / onUpdate 策略:
object OrderTable : Table<Order>("t_order") {
val userId = long("user_id")
.references(
UserTable,
referencedColumn = "id",
onDelete = ForeignKeyAction.CASCADE,
onUpdate = ForeignKeyAction.CASCADE,
)
.bindTo(Order::userId)
}
// 简写版(默认引用 id 列,无级联动作)
val simpleRef = long("user_id").references(UserTable)ForeignKeyAction 枚举:NO_ACTION、RESTRICT、SET_NULL、SET_DEFAULT、CASCADE。
object ArticleTable : Table<Article>("t_article") {
// 静态默认值
val status = varchar("status").default("draft")
// 惰性默认值(每次建表/迁移时重新计算)
val createdAt = varchar("created_at").default { LocalDateTime.now().toString() }
// CURRENT_TIMESTAMP(原生 SQL,不会被引号包裹)
val updatedAt = varchar("updated_at").defaultCurrentTimestamp()
}
.default()会使列变为可空类型:ColumnBuilder<T, R?, W>。
当实体中嵌套了其他对象时,可以使用双参数的 bindTo:
data class Address(val city: String, val street: String)
data class User(
val id: Long = 0,
val address: Address,
)
object UserTable : Table<User>("t_user") {
val id = long("id").primaryKey(autoincrement = true).bindTo(User::id)
val city = varchar("city").bindTo(User::address, Address::city)
val street = varchar("street").bindTo(User::address, Address::street)
}当不需要实体映射时,可以用 ColumnBuilder.invoke() 省略泛型:
object TriggerTable : Table<Any>("trigger") {
val id = long("id").primaryKey(autoincrement = true)
val name = varchar("name")
// 无需 bindTo,不用实体类型
}对于超大结果集,可以使用 iterator() 逐行迭代,支持 use{} 安全提前终止:
database.form(UserTable).selectAll().iterator().use { iter ->
for (row in iter) {
println(row[UserTable.name])
if (someCondition) break // use{} 确保底层 Statement 被关闭
}
}QueryBuilder.deleteAll() 直接基于已有的 WHERE 条件删除,并通知 tracker:
database.form(UserTable).selectAll()
.where { UserTable.status eq "inactive" }
.deleteAll() // 删除所有 status='inactive' 的行Storm 在首次使用数据库时自动创建 storm_meta 表,存储 schema 版本号。可以通过原始 SQL 读取:
SELECT value FROM storm_meta WHERE key = 'schema_version';同时兼容旧的 meta 表,读到旧表数据后自动迁移到 storm_meta。
| 特性 | SQLiteDatabase |
Database |
|---|---|---|
| 职责 | 裸数据库连接包装 | 完整数据库管理 |
| 迁移支持 | 无 | 有(版本管理 + 迁移链) |
| 响应式追踪 | 无 | 有(InvalidationTracker) |
| 事务 | 手动控制 | transaction{} / blockingTransaction{} |
| 关闭 | 不幂等 | 幂等 |
简单场景用 SQLiteDatabase.connect(),需要迁移和响应式就用 Database 或 database{} DSL。
本项目基于 MIT。
如有疑问或建议,请提issues或者写一封邮件发送到zxhhyj@qq.com。