29、数据库与跨平台:KMM中的SQLite与SQLDelight
跨平台开发,说白了就是「一套代码,两边跑」。我做了这么多年Android,最头疼的就是iOS和Android数据层各写一套。业务逻辑一样,数据库操作却要维护两份,改个字段名都得同步改两遍。直到我接触了KMM(Kotlin Multiplatform Mobile),才真正找到了解决方案。
今天我们就聊聊KMM里的数据库怎么玩。我会结合SQLDelight这个框架,讲讲跨平台数据同步、iOS与Android数据互通,最后再跟Realm做个对比。嗯,都是我在项目中踩过的坑和总结的经验。
为什么KMM需要专门的数据库方案?
你想想看,Android用的是SQLite,iOS用的是CoreData或者FMDB。虽然底层都是SQLite,但API完全不同。我在一个项目里试过用expect/actual来抽象数据库操作,结果发现抽象层越写越厚,最后比两套代码还难维护。
KMM的核心价值在于共享业务逻辑。如果数据库操作不能共享,那KMM的意义就少了一半。所以我们需要一个「跨平台的数据库框架」——在共享代码层写SQL,自动生成各平台的实现代码。
SQLDelight:KMM的数据库首选
我个人习惯用SQLDelight。它由Square公司出品,跟Retrofit、OkHttp是同一个家族。它的思路很特别:你在.sq文件里写SQL语句,它自动生成类型安全的Kotlin代码。
核心优势:
- SQL语句在编译期就做语法检查,不会等到运行时才报错
- 自动生成数据类、查询方法、事务管理
- Android端用Android SQLite驱动,iOS端用Native SQLite驱动
- 支持协程、Flow、RxJava
来看一个实际例子。假设我们要做一个笔记应用,先定义数据模型:
// 在 shared/src/commonMain/sqldelight/com/example/Note.sq 文件中
CREATE TABLE Note (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
selectAll:
SELECT * FROM Note ORDER BY updated_at DESC;
selectById:
SELECT * FROM Note WHERE id = ?;
insert:
INSERT INTO Note(title, content, created_at, updated_at) VALUES (?, ?, ?, ?);
update:
UPDATE Note SET title = ?, content = ?, updated_at = ? WHERE id = ?;
delete:
DELETE FROM Note WHERE id = ?;
写完之后,SQLDelight会自动生成NoteQueries类。你可以在共享代码里直接调用:
// 共享代码层
class NoteRepository(private val queries: NoteQueries) {
fun getAllNotes(): Flow<List<Note>> {
return queries.selectAll().asFlow().mapToList()
}
suspend fun addNote(title: String, content: String) {
val now = System.currentTimeMillis()
queries.insert(title, content, now, now)
}
suspend fun updateNote(id: Long, title: String, content: String) {
val now = System.currentTimeMillis()
queries.update(title, content, now, id)
}
}
我的经验:SQLDelight生成的代码是线程安全的。我在一个日活百万的App里用过,从来没出过并发问题。但要注意,.sq文件里的SQL语句必须写标准SQL,不要用Android特有的函数。
跨平台数据库同步:iOS与Android数据互通
数据互通,说白了就是「Android写入的数据,iOS能读到;iOS写入的数据,Android能读到」。这听起来简单,做起来坑不少。
我曾经在一个项目里遇到这样的问题:Android端用Room,iOS端用CoreData,两边数据库结构一样,但数据同步后总是对不上。查了半天,发现是时间戳的精度问题——Android的System.currentTimeMillis()返回毫秒,iOS的Date.timeIntervalSince1970返回秒。这种细节,你不踩坑根本想不到。
用SQLDelight之后,这个问题就简单了。因为两边用的是同一个.sq文件,生成的数据库结构完全一致。数据同步只需要考虑「谁的数据更新」的问题。
推荐的数据同步策略:
- 最后写入者胜出(LWW):每条记录带时间戳,谁的时间戳新就用谁的。简单粗暴,适合大多数场景。
- 增量同步:只同步变更的数据,用updated_at字段做增量标记。我建议用这个,省流量。
- 冲突解决:如果两边同时修改同一条记录,需要定义合并规则。我一般用「服务器版本优先」策略。
来看一个增量同步的代码示例:
// 共享代码层 - 数据同步服务
class SyncService(
private val localQueries: NoteQueries,
private val remoteApi: NoteApi
) {
suspend fun syncNotes() {
// 1. 获取本地最后同步时间
val lastSyncTime = getLastSyncTime()
// 2. 获取本地变更的数据
val localChanges = localQueries.selectChangesSince(lastSyncTime).executeAsList()
// 3. 推送到服务器
remoteApi.pushChanges(localChanges)
// 4. 拉取服务器变更
val remoteChanges = remoteApi.pullChanges(lastSyncTime)
// 5. 合并到本地
remoteChanges.forEach { remoteNote ->
val localNote = localQueries.selectById(remoteNote.id).executeAsOneOrNull()
if (localNote == null || remoteNote.updatedAt > localNote.updatedAt) {
localQueries.insertOrReplace(remoteNote)
}
}
// 6. 更新同步时间
saveLastSyncTime(System.currentTimeMillis())
}
}
注意:千万不要在同步过程中直接操作UI线程。我见过有人直接在Main线程里做数据库同步,结果App直接ANR。同步操作一定要放在后台线程,用协程或者WorkManager调度。
Realm对比:什么时候用SQLDelight,什么时候用Realm?
Realm也是一个跨平台数据库方案,但它跟SQLDelight的思路完全不同。Realm是对象数据库,你直接操作对象,不用写SQL。SQLDelight是关系型数据库,你得写SQL语句。
我两个都用过,说说我的感受:
| 对比维度 | SQLDelight | Realm |
|---|---|---|
| 底层引擎 | SQLite | 自研引擎 |
| 查询方式 | SQL语句 | 链式调用 / 谓词 |
| 类型安全 | 编译期检查 | 运行时检查 |
| 跨平台支持 | KMM原生支持 | KMM支持(但配置复杂) |
| 性能 | 读写均衡 | 读快写慢 |
| 学习成本 | 需要会SQL | 需要学新API |
| 文件大小 | 小(依赖SQLite) | 大(自带引擎) |
我个人建议:
- 选SQLDelight的场景:你的团队熟悉SQL,需要复杂查询、事务、关联表。或者你的App已经有SQLite数据,需要迁移到KMM。
- 选Realm的场景:你的数据结构简单,不想写SQL,追求快速开发。或者你需要Realm的实时同步功能(Realm Sync)。
避坑指南:我曾经在一个项目里用Realm做跨平台同步,结果发现Realm的KMM支持当时还不成熟,配置起来非常痛苦。后来换成了SQLDelight,一周就搞定了。所以如果你现在开始做KMM项目,我建议优先考虑SQLDelight。
总结一下
KMM的数据库方案,SQLDelight是目前最成熟的选择。它让你在共享代码层写SQL,自动生成各平台的实现代码。数据同步方面,用增量同步加最后写入者胜出策略,基本能覆盖大多数场景。至于Realm,它有自己的优势,但目前在KMM生态里还不够完善。
嗯,今天就聊到这里。如果你在实际项目中遇到数据库跨平台的问题,欢迎交流。记住一点:没有最好的框架,只有最适合你项目的方案。
公众号:蓝海资料掘金营,微信deep3321