🗄️ SQLDelight 简介
SQLDelight 是 Square 开发的 KMP 数据库库,其核心思想是以 SQL 为第一公民:你先写 SQL 语句,SQLDelight 自动生成对应的类型安全 Kotlin API。
- Schema 优先 在 .sq 文件中用标准 SQL 编写建表语句和查询语句,SQLDelight 在编译时解析这些 SQL 并生成对应的 Kotlin 接口代码,包含编译期类型检查。
- 类型安全 生成的 Kotlin API 完全类型安全。例如 INSERT 语句会生成一个接受正确类型参数的函数,避免运行时 SQL 错误。
- Flow 集成 通过 coroutines-extensions 扩展包,SELECT 查询可以返回 Flow,数据库内容变化时 Flow 自动发射新值,天然支持响应式 UI。
- 多平台驱动 同一套 API,Android 使用 AndroidSqliteDriver,iOS 使用 NativeSqliteDriver(基于 SQLite3 C 库),Desktop 使用 JdbcSqliteDriver。
SQLDelight vs Room: Room 是 Android 专属(JVM only),SQLDelight 支持 KMP 所有目标平台。如果你的项目只面向 Android,Room 也是很好的选择;如果需要跨平台,选 SQLDelight。
⚙️ 依赖与插件配置
shared/build.gradle.kts
plugins {
alias(libs.plugins.sqldelight)
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.sqldelight.runtime)
implementation(libs.sqldelight.coroutines.extensions) // Flow 支持
}
androidMain.dependencies {
implementation(libs.sqldelight.android.driver)
}
iosMain.dependencies {
implementation(libs.sqldelight.native.driver)
}
}
}
// SQLDelight 插件配置
sqldelight {
databases {
create("AppDatabase") {
// 生成代码的包名
packageName.set("com.example.newsapp.database")
// 数据库版本(迁移时需要递增)
version = 1
}
}
}
.sq 文件位置: SQLDelight 会在
shared/src/commonMain/sqldelight/ 目录下查找 .sq 文件,注意路径中要包含包名目录结构,如 sqldelight/com/example/newsapp/database/。
📝 Schema 定义(.sq 文件)
5.1 建表语句
commonMain/sqldelight/com/example/newsapp/database/Article.sq
CREATE TABLE ArticleEntity (
id TEXT NOT NULL PRIMARY KEY,
title TEXT NOT NULL,
summary TEXT NOT NULL,
cover_url TEXT,
author_name TEXT NOT NULL,
author_avatar TEXT,
published_at INTEGER NOT NULL, -- 存储 Unix 时间戳(毫秒)
is_bookmarked INTEGER NOT NULL DEFAULT 0, -- SQLite 中布尔值用 0/1
created_locally INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
);
CREATE INDEX article_published_at ON ArticleEntity(published_at);
5.2 查询语句
Article.sq — 查询语句定义
-- 查询名:getAll(生成 queries.getAll() 函数)
getAll:
SELECT * FROM ArticleEntity
ORDER BY published_at DESC;
-- 带分页
getPage:
SELECT * FROM ArticleEntity
ORDER BY published_at DESC
LIMIT :pageSize OFFSET :offset;
-- 根据 id 查询
getById:
SELECT * FROM ArticleEntity
WHERE id = :id;
-- 搜索
search:
SELECT * FROM ArticleEntity
WHERE title LIKE '%' || :query || '%'
OR summary LIKE '%' || :query || '%'
ORDER BY published_at DESC;
-- 仅获取书签
getBookmarked:
SELECT * FROM ArticleEntity
WHERE is_bookmarked = 1
ORDER BY published_at DESC;
-- 插入或替换(UPSERT)
upsert:
INSERT OR REPLACE INTO ArticleEntity
(id, title, summary, cover_url, author_name, author_avatar, published_at)
VALUES (:id, :title, :summary, :coverUrl, :authorName, :authorAvatar, :publishedAt);
-- 切换书签状态
toggleBookmark:
UPDATE ArticleEntity
SET is_bookmarked = CASE WHEN is_bookmarked = 0 THEN 1 ELSE 0 END
WHERE id = :id;
-- 删除旧数据(只保留最近 100 条)
deleteOld:
DELETE FROM ArticleEntity
WHERE id NOT IN (
SELECT id FROM ArticleEntity
ORDER BY published_at DESC
LIMIT 100
);
⚡ 生成的 API
SQLDelight 在编译时根据 .sq 文件生成如下代码(你不需要手写,只是了解生成结果):
生成代码示意(实际由 SQLDelight 自动生成)
// AppDatabase — 数据库入口
interface AppDatabase {
val articleQueries: ArticleQueries
companion object {
operator fun invoke(driver: SqlDriver): AppDatabase
val Schema: SqlSchema<QueryResult.AsyncValue<Unit>>
}
}
// ArticleQueries — 所有查询的类型安全接口
interface ArticleQueries : SuspendingTransacter {
fun getAll(): Query<ArticleEntity>
fun <T> getAll(mapper: (..fields..) -> T): Query<T>
fun getById(id: String): Query<ArticleEntity>
suspend fun upsert(
id: String, title: String, summary: String,
coverUrl: String?, authorName: String,
authorAvatar: String?, publishedAt: Long
)
suspend fun toggleBookmark(id: String)
suspend fun deleteOld()
}
5.3 使用 Flow 监听变化
commonMain — Flow 查询
import app.cash.sqldelight.coroutines.*
import kotlinx.coroutines.*
class NewsLocalDataSource(private val db: AppDatabase) {
// 返回 Flow,数据库写入时自动通知
fun getAllArticles(): Flow<List<ArticleEntity>> =
db.articleQueries.getAll()
.asFlow() // 转 Flow
.mapToList(Dispatchers.IO) // 在 IO 线程执行查询
fun getBookmarkedArticles(): Flow<List<ArticleEntity>> =
db.articleQueries.getBookmarked()
.asFlow()
.mapToList(Dispatchers.IO)
// 批量 UPSERT(在事务中执行,性能更好)
suspend fun upsertAll(articles: List<ArticleEntity>) {
db.transactionWithContext(Dispatchers.IO) {
articles.forEach { article ->
db.articleQueries.upsert(
id = article.id,
title = article.title,
summary = article.summary,
coverUrl = article.cover_url,
authorName = article.author_name,
authorAvatar = article.author_avatar,
publishedAt = article.published_at
)
}
}
// 清理超出 100 条的旧数据
db.articleQueries.deleteOld()
}
suspend fun toggleBookmark(id: String) {
db.articleQueries.toggleBookmark(id)
}
}
🔌 多平台驱动
5.4 expect/actual 创建驱动
commonMain
expect fun createDatabaseDriver(name: String): SqlDriver
androidMain
import app.cash.sqldelight.driver.android.*
// Android 需要 Context,通过 Koin 注入
actual fun createDatabaseDriver(name: String): SqlDriver {
val context = androidContext() // Koin androidContext()
return AndroidSqliteDriver(
schema = AppDatabase.Schema,
context = context,
name = name // 文件名,如 "news.db"
)
}
iosMain
import app.cash.sqldelight.driver.native.*
actual fun createDatabaseDriver(name: String): SqlDriver =
NativeSqliteDriver(AppDatabase.Schema, name)
5.5 在 Koin 中注册
commonMain — Koin 模块
val databaseModule = module {
single<SqlDriver> { createDatabaseDriver("news.db") }
single { AppDatabase(get()) }
single { NewsLocalDataSource(get()) }
}
📴 离线缓存模式
5.6 Single Source of Truth 模式
推荐的离线缓存设计:数据库是唯一数据来源(SSOT),UI 只订阅数据库的 Flow,网络数据写入数据库后 Flow 自动更新。
commonMain — Repository SSOT 实现
class NewsRepositoryImpl(
private val api: NewsApi,
private val localDataSource: NewsLocalDataSource
) : NewsRepository {
// UI 只从数据库订阅——永远不会无数据显示
override fun getArticles(): Flow<List<Article>> =
localDataSource.getAllArticles()
.map { entities -> entities.map { it.toDomain() } }
// 刷新:拉取网络数据写入数据库,数据库变化自动触发 Flow 更新
override suspend fun refreshArticles() {
val remoteDtos = api.getArticles().data
localDataSource.upsertAll(remoteDtos.map { it.toEntity() })
}
}
ViewModel 中触发刷新
class HomeViewModel(private val repository: NewsRepository) : ViewModel() {
private val _isRefreshing = MutableStateFlow(false)
val uiState: StateFlow<HomeUiState> = combine(
repository.getArticles(),
_isRefreshing
) { articles, isRefreshing ->
HomeUiState(articles = articles, isRefreshing = isRefreshing)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), HomeUiState())
fun refresh() {
viewModelScope.launch {
_isRefreshing.value = true
try { repository.refreshArticles() }
catch (e: ApiException) { /* 处理错误 */ }
finally { _isRefreshing.value = false }
}
}
}
🔄 数据库迁移
5.7 创建迁移文件
当需要修改数据库 Schema 时,必须创建迁移文件,避免用户升级 App 时数据丢失。
目录结构
commonMain/sqldelight/
└── com/example/newsapp/database/
├── Article.sq # 当前 Schema
└── migrations/
└── 1.sqm # 版本 1→2 的迁移脚本
migrations/1.sqm — 添加新字段
-- 迁移:版本 1 → 版本 2
-- 为 ArticleEntity 添加 view_count 字段
ALTER TABLE ArticleEntity ADD COLUMN view_count INTEGER NOT NULL DEFAULT 0;
-- 添加新表
CREATE TABLE TagEntity (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL
);
build.gradle.kts — 更新版本号
sqldelight {
databases {
create("AppDatabase") {
packageName.set("com.example.newsapp.database")
version = 2 // 从 1 改为 2
}
}
}
迁移测试: 每次发布数据库迁移,务必测试从老版本升级的场景,不能只测试全新安装。SQLDelight 提供 migrationVerification 工具帮助验证迁移脚本的正确性。
5.8 自定义列类型适配器
SQLite 原生只支持 INTEGER/REAL/TEXT/BLOB,对于 Kotlin 特殊类型需要适配器:
commonMain — 自定义 ColumnAdapter
import app.cash.sqldelight.*
// Instant 存储为 Long(Unix 毫秒)
val instantAdapter = object : ColumnAdapter<Instant, Long> {
override fun decode(databaseValue: Long) =
Instant.fromEpochMilliseconds(databaseValue)
override fun encode(value: Instant) =
value.toEpochMilliseconds()
}
// List<String> 存储为逗号分隔字符串
val stringListAdapter = object : ColumnAdapter<List<String>, String> {
override fun decode(databaseValue: String) =
databaseValue.split(",").filter { it.isNotBlank() }
override fun encode(value: List<String>) =
value.joinToString(",")
}
// 创建数据库时传入适配器
val database = AppDatabase(
driver = driver,
ArticleEntityAdapter = ArticleEntity.Adapter(
published_atAdapter = instantAdapter,
tagsAdapter = stringListAdapter
)
)
🎯 实践任务
- 创建
Article.sq文件,定义建表语句和 getAll/upsert/toggleBookmark 查询 - 用 expect/actual 实现
createDatabaseDriver() - 封装
NewsLocalDataSource,提供getAllArticles(): Flow方法 - 在 Repository 中实现 Single Source of Truth 模式