Skip to content

kaus-io/storm

Repository files navigation

项目介绍

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)
}

驱动区别详见 驱动 章节。

快速上手

这里以用户表和用户信息表为示例:

1.定义主表

创建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来定义列。

2.定义关联表

创建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)
}

3.连接数据库

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)

4.使用 Database 访问 Database

SQLiteDatabase提供了insertupdatedeleteform/entities等API。

1.插入数据

// 实体插入(返回自增 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 = "覆盖更新"))

2.删除数据

// 删除全部
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 "张三" }

3.更新数据

database.update(UserTable)
    .set { this[UserTable.name] = "new name" }
    .where { UserTable.id eq 1 }
    .execute()

// 更新全部
database.updateAll(UserTable) { this[UserTable.info] = "无限进步" }

4.使用 entities API 查询实体

需要 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()

5.使用 form API 查询数据

适用于部分列查询和 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
    }
}

6.使用 JOIN 查询

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)

7.使用聚合函数

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 行的计数

8.使用事务

// 挂起事务(推荐,自动追踪脏表)
database.transaction {
    insert(UserTable, User(name = "张三"))
    insert(UserInfoTable, UserInfo(email = "zxhhyj@qq.com"))
}
// 提交后自动通知 observe 观察者

// 阻塞事务(非协程环境)
database.blockingTransaction {
    database.execSQL("INSERT ...")
}

9.响应式查询

当数据变更时自动重新查询,无需手动轮询。支持两种方式:

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(事务提交后,两次插入合并为一次)

5.定义并初始化数据库

创建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 一致性。

6.使用 SQLiteConnection 多数据源

参考连接数据库,创建不同的SQLiteConnection即可。

7.快速生成 SQL

使用中缀 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 DatabaseSQLiteConnectionTableColumn、CRUD、迁移、observe 响应式查询 非实体
storm-orm entities()EntityBuilderpacking()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 官方库:

内置 SQLite 驱动(Bundled)

模块:com.zxhhyj:storm-driver-androidx-sqlite-bundled

封装 androidx.sqlite:sqlite-bundled,底层为从源码编译的内置 SQLite 库,各平台版本完全一致。

特点

  • 支持 Kotlin MultiplatformAndroid / 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)

框架 SQLite 驱动(Framework)

模块: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-bundledstorm-driver-androidx-sqlite-framework
iOS / macOS / Linux 平台项目,使用平台 SQLite storm-driver-androidx-sqlite-framework
需要在查询路径中绑定 Blob 两者均支持
JVM Desktop / Server storm-driver-androidx-sqlite-bundled

更多用法

下面整理了一些 Storm 提供的实用能力,覆盖从 Schema 定义到查询执行的多个环节,日常开发中可能会用到。

1. RowBuilder 手动构建行数据

如果不想定义实体类,或者字段需要做复杂转换(比如密封类/多态),可以直接用 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)

2. 类型转换器 — transform() 列级类型映射

将任意 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) },
        )
    )
}

内置转换器:BooleanTypeConvertersDateTypeConvertersTimestampTypeConverters

Storm 还预置了便捷列类型:

  • boolean(name) — 存储为 Long(0/1)
  • date(name) / datetime(name) — 存储为 ISO-8601 String
  • enum(name) — 存储为枚举 name 字符串

3. CHECK 约束 DSL

在列定义时直接编写 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), ...)

4. 外键动作 — references() 的深层用法

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_ACTIONRESTRICTSET_NULLSET_DEFAULTCASCADE

5. 默认值的高级用法

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>

6. 嵌套属性绑定

当实体中嵌套了其他对象时,可以使用双参数的 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)
}

7. 无需实体类型的列定义

当不需要实体映射时,可以用 ColumnBuilder.invoke() 省略泛型:

object TriggerTable : Table<Any>("trigger") {
    val id = long("id").primaryKey(autoincrement = true)
    val name = varchar("name")
    // 无需 bindTo,不用实体类型
}

8. QueryBuilder 的 iterator 迭代器

对于超大结果集,可以使用 iterator() 逐行迭代,支持 use{} 安全提前终止:

database.form(UserTable).selectAll().iterator().use { iter ->
    for (row in iter) {
        println(row[UserTable.name])
        if (someCondition) break  // use{} 确保底层 Statement 被关闭
    }
}

9. 基于查询条件的删除

QueryBuilder.deleteAll() 直接基于已有的 WHERE 条件删除,并通知 tracker:

database.form(UserTable).selectAll()
    .where { UserTable.status eq "inactive" }
    .deleteAll()  // 删除所有 status='inactive' 的行

10. 自省 version 信息

Storm 在首次使用数据库时自动创建 storm_meta 表,存储 schema 版本号。可以通过原始 SQL 读取:

SELECT value FROM storm_meta WHERE key = 'schema_version';

同时兼容旧的 meta 表,读到旧表数据后自动迁移到 storm_meta

11. DatabaseSQLiteDatabase 的区别

特性 SQLiteDatabase Database
职责 裸数据库连接包装 完整数据库管理
迁移支持 有(版本管理 + 迁移链)
响应式追踪 有(InvalidationTracker
事务 手动控制 transaction{} / blockingTransaction{}
关闭 不幂等 幂等

简单场景用 SQLiteDatabase.connect(),需要迁移和响应式就用 Databasedatabase{} DSL。

开源协议

本项目基于 MIT

交流

如有疑问或建议,请提issues或者写一封邮件发送到zxhhyj@qq.com

About

Storm是直接基于纯Kotlin编写的高效简洁的轻量级Kotlin Multiplatform SQL ORM框架,提供了强类型的 SQL DSL,直接将低级bug暴露在编译期。

Resources

License

Stars

Watchers

Forks

Contributors

Languages