Skip to content

Repository files navigation

sqliteorz

一个轻量、无注解的 Android SQLite ORM,适合学习、原型和数据模型较简单的应用。 sqliteorz 基于 SQLiteOpenHelper 和反射提供建表、对象映射、CRUD、事务批量写入、 多数据库和版本迁移能力。

sqliteorz 的目标是保持简单。如果项目需要关系映射、复杂查询、响应式数据流或编译期 SQL 校验,建议使用 Room。

特性

  • 模型继承 DBBaseModel 即可使用,无需注解或代码生成。
  • 自动创建数据表,内置 idcreateTimeupdateTime 字段。
  • 支持单条及批量写入、条件查询、排序、分页、更新、删除和计数。
  • 查询条件通过 whereArgs 绑定,更新值通过 ContentValues 绑定。
  • 批量写入自动分批并使用事务,支持 ABORTIGNOREREPLACE 冲突策略。
  • 支持延迟获取 Context、账号和数据库配置,首次 CRUD 时自动初始化并可按账号切库。
  • 提供 DBResult 非抛异常调用方式,明确区分未就绪、成功和执行失败。
  • 支持多个数据库、非破坏性迁移及显式破坏性重建。
  • 最低支持 Android 5.0(API 21),库本身没有第三方运行时依赖。

引入

在项目的 settings.gradle 中加入 JitPack:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url "https://jitpack.io" }
    }
}

在应用模块中添加依赖:

dependencies {
    implementation "com.github.lawnvi:sqliteorz:2.0.1"
}

快速开始

1. 定义模型

模型必须继承 DBBaseModel,并提供可访问的无参构造函数。Kotlin 构造参数都有默认值时, 编译器会生成可用的无参构造函数。

data class User(
    var name: String = "",
    var age: Int = 0,
    var enabled: Boolean = true,
) : DBBaseModel() {
    override fun getTableName() = "users"
}

建议显式覆写 getTableName(),避免包名或类名变化影响已有表名。

支持以下字段类型及对应的可空类型:

Kotlin 类型 SQLite 类型
IntShortLongBoolean INTEGER
FloatDouble REAL
CharString TEXT
ByteArray BLOB

2. 初始化

建议在 Application.onCreate() 中注册模型并初始化:

val driver = DBDriver(
    name = "app.db",
    version = 1,
).apply {
    registerModel(User::class.java)
}

OrzHelper.initialize(
    applicationContext,
    drivers = arrayOf(driver),
)

数据库默认保存在应用内部数据库目录,不需要存储权限。

延迟初始化

在 Xposed 等早于宿主 Application 创建的场景,可以先注册配置提供者:

OrzHelper.prepare(
    keyProvider = {
        currentAccountIdOrNull()?.let { "$it:schema-v2" }
    },
    configurationFactory = factory@{ capturedKey ->
        val context = currentApplicationOrNull() ?: return@factory null
        val accountId = capturedKey.removeSuffix(":schema-v2")
        val driver = DBDriver(name = "$accountId.db", version = 2).apply {
            registerModel(User::class.java)
        }
        OrzConfiguration(
            context = context,
            drivers = arrayOf(driver),
        )
    },
)

keyProvider 会在 CRUD 前执行,应保持轻量且线程安全;只有 key 变化时才会调用 configurationFactory,并将已捕获的 key 传给它创建 Driver。任一提供者返回 null 时,本次访问抛出 DatabaseNotReadyException,已打开的旧库会保留但不会用于本次操作,后续访问仍可重试。

新配置成功创建后才会关闭旧库并切换。key 应同时包含账号标识和数据库配置版本; 配置未变化时必须保持同一个 key。账号注销时显式调用 OrzHelper.invalidate() 关闭旧库, 它会保留延迟配置,后续登录仍可自动初始化。

切换前会真正打开新数据库并完成建表或迁移;打开失败时旧数据库保持可恢复状态, 当前操作返回失败,不会退回旧账号继续执行。

也可以主动调用 OrzHelper.ensureInitialized() 获取是否已就绪,或用 OrzHelper.hasRetainedState() 检查是否保留了数据库实例。直接调用 initialize() 会改用立即初始化配置。

3. 保存与查询

val user = User(name = "张三", age = 20)
check(user.save())

val adults = DBOrz.findModels<User>(
    where = "age >= ? AND enabled = ?",
    whereArgs = arrayOf("18", "1"),
    orderMap = linkedMapOf("age" to -1),
)

val userById = DBOrz.findOne<User>(id = user.id)
val count = DBOrz.count<User>(
    where = "age >= ?",
    whereArgs = arrayOf("18"),
)

为兼容旧版,单条插入发生 SQLite 错误时 save() 返回 false。需要明确异常时使用 saveOrThrow();需要结果对象时使用 runDbCatching { user.saveOrThrow() }

orderMap 的值大于 0 表示升序,其他值表示降序。查询全部数据时显式使用 where = "1=1"

分页查询:

val page = DBOrz.findModels<User>(
    where = "1=1",
    orderMap = linkedMapOf("id" to -1),
    limit = 20,
    offset = 40,
)

所有外部输入都应通过 ?whereArgs 绑定,不要直接拼接到 SQL 中。

4. 更新与删除

已保存的模型可以直接操作当前记录:

user.update(mapOf("name" to "李四", "enabled" to false))
user.delete()

也可以按条件批量操作:

DBOrz.update<User>(
    setMaps = mapOf("enabled" to false),
    where = "age < ?",
    whereArgs = arrayOf("18"),
)

DBOrz.delete<User>(
    where = "enabled = ?",
    whereArgs = arrayOf("0"),
)

为避免误操作,更新和删除全部数据时必须显式传入 where = "1=1"

5. 批量保存

val users = List(1_000) { index ->
    User(name = "user-$index", age = 18 + index % 50)
}

DBOrz.saveModels(users)

批量数据会先校验全部元素,再按每 500 条一组执行事务。同一批数据必须是相同 Model、 相同数据表并属于同一数据库。默认使用 ABORT,冲突时回滚当前事务并抛出 DBException

需要跳过冲突数据或替换旧数据时,显式指定策略:

val result = DBOrz.saveModels(users, ConflictStrategy.IGNORE)
println("写入 ${result.successCount} 条,忽略 ${result.ignoredCount}")

DBOrz.saveModels(users, ConflictStrategy.REPLACE)

IGNORE 会跳过发生约束冲突的记录并继续写入;REPLACE 使用 SQLite 的替换语义, 可能删除冲突行后插入新行,因此主键可能变化。

非抛异常调用

除兼容旧行为的 save() 外,严格 CRUD 会在数据库错误时抛出 DBException。 不希望 ORM 异常进入宿主进程时,使用 runDbCatching 包裹数据库操作:

when (val result = runDbCatching { DBOrz.findModels<User>() }) {
    is DBResult.Success -> showUsers(result.value)
    DBResult.DatabaseNotReady -> waitForAccountOrApplication()
    is DBResult.Failure -> reportDatabaseError(result.error)
}

查询不到数据属于 Success(emptyList()),只有数据库未就绪才返回 DatabaseNotReady,其他初始化或 SQLite 错误返回 Failure。业务异常、线程中断和取消 不会被包装,应由调用方按自身逻辑处理。

多数据库

每个模型只能注册到一个数据库。配置多个数据库时,需要显式指定默认数据库:

val mainDriver = DBDriver("main.db").apply {
    registerModel(User::class.java)
}
val cacheDriver = DBDriver("cache.db").apply {
    registerModel(CacheEntry::class.java)
}

OrzHelper.initialize(
    applicationContext,
    default = "main.db",
    drivers = arrayOf(mainDriver, cacheDriver),
)

sqliteorz 会根据模型注册关系自动选择对应数据库。

需要直接调用 SQLite API 时,把完整操作放在受保护的代码块内,避免账号切换关闭正在使用的库:

OrzHelper.withDatabase("main.db") { helper ->
    helper.writableDatabase.execSQL("CREATE INDEX IF NOT EXISTS user_age ON users(age)")
}

use()model() 仅为兼容旧代码保留,不应长期持有其返回的 DBHelper

数据库升级

数据库版本增加时应注册迁移;没有迁移策略会直接抛出异常,避免静默丢失数据:

val driver = DBDriver(name = "app.db", version = 2)
    .migrateWith { db, oldVersion, _ ->
        if (oldVersion < 2) {
            db.execSQL("ALTER TABLE users ADD COLUMN email TEXT")
        }
    }

driver.registerModel(User::class.java)

仅对缓存等允许丢失数据的数据库启用破坏性重建:

driver.fallbackToDestructiveMigration()

字段重命名、删除、类型变化及索引变更都需要在迁移中自行处理。

自定义字段转换

需要加密、压缩等自定义写入逻辑时,可以覆写 toContentValues()

override fun toContentValues() = super.toContentValues().apply {
    put("content", encrypt(content))
}

如果同时需要自定义读取逻辑,可覆写 cursor2Model(),并确保读写格式一致。

从 1.x 升级

  • 原来的 fieldStr()valueStr() 扩展点应迁移到 toContentValues()
  • 模型主键 idInt 调整为 Long,使用主键变量的业务代码需要同步修改类型。
  • ByteArray 新数据使用原生 SQLite BLOB,读取时兼容旧版 Base64 文本。
  • 默认数据库改用应用内部数据库目录;检测到旧的应用专属外部目录数据库时会继续使用旧路径。
  • 升级前请备份真实业务数据库,并为每个数据库版本变化提供迁移。

已知限制

  • 主键使用 SQLite INTEGER PRIMARY KEY,在模型中映射为 Long
  • 表结构和对象映射依赖运行时反射,没有编译期字段或 SQL 校验。
  • 不提供表关联、对象关系、LiveData、Flow 或跨进程访问能力。
  • 查询条件使用 SQLite selection 字符串,复杂查询可直接使用原生 SQLite 或改用 Room。

About

a simple sqlite orm

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Contributors

Languages