29、数据库与跨平台:KMM中的SQLite与SQLDelight

跨平台开发,说白了就是「一套代码,两边跑」。我做了这么多年Android,最头疼的就是iOS和Android数据层各写一套。业务逻辑一样,数据库操作却要维护两份,改个字段名都得同步改两遍。直到我接触了KMM(Kotlin Multiplatform Mobile),才真正找到了解决方案。

今天我们就聊聊KMM里的数据库怎么玩。我会结合SQLDelight这个框架,讲讲跨平台数据同步、iOS与Android数据互通,最后再跟Realm做个对比。嗯,都是我在项目中踩过的坑和总结的经验。

KMM 跨平台数据库知识体系 KMM 数据库 SQLDelight 框架 原生 SQLite Realm 对比 SQL 定义在 .sq 文件 自动生成 Kotlin 代码 跨平台查询 Android: Room / 原生 SQLite iOS: CoreData / FMDB 数据同步方案 对象数据库 性能对比 适用场景

为什么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