KMM 入门使用 SQLDelight 操作数据库
Posted 袁国正_yy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了KMM 入门使用 SQLDelight 操作数据库相关的知识,希望对你有一定的参考价值。
数据库在 App 中的作用
移动 App 的数据库与 Server 数据库不同,其主要目的是为了缓存一些数据,如:历史消息、数据打点、列表数据缓存等,宗旨都是为优化用户体验建立一套简单的数据基础
由于 SQLite 完全开源,且比较轻量(不需要像 mysql 这样建立一个单独的进程,直接操作 DB 文件),目前,在各类移动端操作系统(包括不限于 android、ios、Windows)当中,都会内置 SQLite,以便开发者存取结构化数据
于是,围绕 SQLite 展开的开发框架也越来越多,比如:iOS 上的 FMDB、以及 Apple 官方的 CoreData,Android 上的 SQLiteOpenHelper,以及基于它构建的 GreenDAO、Android 官方的 Room 等等,这些框架使得开发者不需要关注 SQLite 中 C/C++ 一层的 API,大幅降低了移动端数据库的开发成本,使得数据存取变得容易
虽然 SQLite 用途广泛,但 SQLite 也存在着一些性能问题,这些性能问题在数据量比较庞大时,体现地更为明显,近几年也出现了一些面向移动端,基于 NoSQL 或对 SQLite 进行改进数据库框架,如:Realm、WCDB……
那么在 KMM 中,如果需要操作数据库,使用 SQLDelight 框架,无疑是目前比较好的选择
SQLDeilight 简介及特点
SQLDelight 由 Square (开发过 OkHttp、Retrofit、LeakCanary 等一些著名的框架)发起,起初是应用在 Cash App 上,完全使用 Kotlin 进行开发,利用 Kotlin Native 的特性,可以实现 Android、iOS、macOS、Windows 等平台,或 JVM、javascript 运行时上的 SQLite 读写操作,也可以借助 JVM,实现对 MySQL、PostgreSQL、HSQL/H2 数据库的支持
利用 SQLDelight 开发的大体思路是先使用 SQL 语句构建表结构和基本的 CURD 操作,根据开发人员编写的 SQL 语句,通过 IDEA 上的 Plugin 进行扫描和解析,从而生成用于建表、迁移及读写数据的 Kotlin 代码,于是在 KMM 模块的 Common 目录中便可调用相关的 CURD 方法,实现对数据库的增删改查
另外,由于双端底层实现的差异(Android 基于 JVM,iOS 基于 Kotlin Native),需要借助 expect/actual
来注入实际的 SQLiteDriver 并进行初始化,才可以正常使用 SQLite 数据库
SQLDelight 框架实现对 SQLite 的基本操作流程,如下图所示
![](https://img.coderyuan.com/1623844026346.png)
由于其只需要编写符合 SQLite 规范的 SQL 脚本文件(.sq)即可自动生成对应的 Entity、DAO 等,在一定程度上比 Android 上已有的一些框架使用起来还更为简单、方便
使用 SQLDelight 开发的流程
注意:本文使用的是 SQLDelight 1.5.0 版本,需要搭配 Gradle 6.8 及以上的版本使用,使用较低的版本会导致报错!
插件安装
想要使用 SQLDelight 进行开发,首先需要安装官方推出的插件,同 KMM 插件一样,首先需要进入到 Android Studio 设置的『Plugins』页面,只需要在『Marketplace』Tab 中搜索 SQLDelight,根据提示安装,然后重启 Android Studio 即可
![](https://img.coderyuan.com/1623845895101.png)
它的源码目录链接为:https://github.com/cashapp/sqldelight/tree/master/sqldelight-idea-plugin,如果有兴趣还可以自己手动编译插件
添加 SQLDelight 依赖
由于需要引入 SQLDelight 的 Gradle 插件,所以首先需要在工程根目录的 build.gradle
(或 kts)文件中,加入 SQLDelight 插件依赖
// build.gradle(根目录)
buildscript
// ...
repositories
google()
mavenCentral()
dependencies
// 需要在 dependencies 闭包中加入如下依赖
classpath 'com.squareup.sqldelight:gradle-plugin:1.5.0'
待 Gradle Sync 完成以后,再到 KMM 模块的 build.gradle.kts
文件中,依赖 SQLDelight 插件、主库、Driver 等,同时进行基本配置
// build.gradle.kts(KMM 模块目录中)
plugins
kotlin("multiplatform")
id("com.android.library")
// 1. 在 plugins 闭包中加入以下依赖
id("com.squareup.sqldelight")
// ...
kotlin
// ...
sourceSets
// ...
val androidMain by getting
dependencies
// 2. 在 androidMain 后面的闭包中,加入 Android 平台的数据库驱动依赖(Android)
implementation("com.squareup.sqldelight:android-driver:1.5.0")
val iosMain by getting
dependencies
// 3. 在 iosMain 后面的闭包中,加入 iOS 平台的数据库驱动依赖(Native)
implementation("com.squareup.sqldelight:native-driver:1.5.0")
// ...
// 4. 与 kotlin 闭包同级,加入 sqldelight 闭包,配置数据库基本信息
sqldelight
// database 方法的首个参数为数据库名称,数据库文件、入口类的命名都以此为准
database("MyKmmAppDB")
// packageName 为生成 SQLite 操作类的包名,根据情况合理指定即可
packageName = "com.coderyuan.kmm"
// 除包名外,还可以配置生成的类文件目录、sq 文件路径、迁移文件等等,这里先不做过多介绍
创建 sq 文件目录
这个目录的作用是用来存放数据库表结构及 CRUD 操作的 SQL 语句文件,以便 SQLDelight 插件能够扫描到并自动生成对应的 Kotlin 类文件
其默认的创建规则如下(如在 Gradle 中进行了特殊配置,需要根据配置修改路径):
- 在 commonMain 目录中,与 kotlin 目录平级,取名为:sqldelight
- 需要创建类似 Java/Kotlin 的包结构目录,如:
com/coderyuan/kmm
,包名要与 Gradle 中配置的包名相符,否则会在编译时报错
![](https://img.coderyuan.com/1623849422979.png)
建议 sq 文件中不要声明过多的表结构,sq 文件名不强制要求与表名一致,但应该按照业务命名,提升可读性,减少维护成本
![](https://img.coderyuan.com/1625818155887.png)
CREATE TABLE User (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -- 用户 ID
name TEXT, -- 用户名
age INTEGER, -- 年龄
gender INTEGER, -- 性别
phoneNumber TEXT -- 电话
);
insertUser:
INSERT INTO User(name, age, gender, phoneNumber) VALUES(?,?,?,?);
queryById:
SELECT * FROM User WHERE id = ?;
比如创建一个以上的 User 表(具体语法可以参考 SQLite 的相关资料)附带插入和查询两个方法,如果 Android Studio 上已经安装了 SQLDelight 插件,此时按下保存(Command + S)即可触发类文件的生成
生成数据库操作类文件
确保 sq 文件中没有语法错误后,可以试着构建一下工程,在使用默认配置的情况下,如果没有错误,会在 KMM 模块的 build/generated/sqldelight
目录中生成类似下图中的几个和数据库操作相关的文件
![](https://img.coderyuan.com/1623855855842.png)
如果执行 ./gradlew assembleDebug
进行构建时,提示有如下的错误,大概率是需要升级 Gradle 的版本
Caused by: java.lang.NoSuchMethodError: kotlin.jvm.internal.FunctionReferenceImpl.<init>(ILjava/lang/Class;Ljava/lang/String;Ljava/lang/String;I)V
以我现在使用的 Android Studio 4.2.1 版本为例,用它创建的 Android 工程,默认会使用 Gradle 6.7.1,而 SQLDelight 1.5.0 需要使用 Gradle 6.8,不过部分工程可能因为历史原因,不能直接将 Gradle 版本升级,此时或许需要考虑适当对 SQLDelight 降级
![](https://img.coderyuan.com/1623858577296.png)
配置数据库 Driver 并进行初始化
由于 SQLDelight 对于双端的底层能力实现不同,在完成上面的操作以后,也只是生成了基本的 CRUD 方法,想让数据库真正 Run 起来,还得为其配置 SQLite Driver,并进行初始化操作
首先需要在 Common 模块中定义一个 DBManager 单例用来 Hold 数据库的连接及一系列数据库 Query 的 Transacter 实现
// DBManager.kt (Common)
package com.example.mykmmapp
import com.squareup.sqldelight.db.SqlDriver
const val DB_NAME = "MyKMMAppDB.db" // 数据库实际 db 文件名
// 创建、存储 Transacter 的单例
expect object DBManager
fun getInstance(): MyKmmAppDB?
// Schema 是一套数据库操作的 API
object Schema : SqlDriver.Schema by MyKmmAppDB.Schema
override fun create(driver: SqlDriver)
MyKmmAppDB.Schema.create(driver)
完成以上定义后,由于底层 Driver 的差异,需要在 androidMain
和 iosMan
中分别实现 Driver 的初始化代码
首先是 Android 中的实现
// DBManager.kt (Android)
package com.example.mykmmapp
import android.content.Context
import com.squareup.sqldelight.android.AndroidSqliteDriver
import com.squareup.sqldelight.db.SqlDriver
import java.lang.ref.WeakReference
actual object DBManager
// 使用弱引用引用 Context,防止内存泄露
// 也可以考虑使用自建工具类中的 Application Context
var contextRef = WeakReference<Context?>(null)
private var driverRef: SqlDriver? = null
private var dbRef: MyKmmAppDB? = null
private val ready: Boolean
get() = driverRef != null
private fun dbSetup(driver: SqlDriver)
val db = MyKmmAppDB(driver)
driverRef = driver
dbRef = db
// clear 可在适当的时间调用,释放内存
fun dbClear()
driverRef?.close()
dbRef = null
driverRef = null
contextRef = null
@JvmStatic
actual fun getInstance(): MyKmmAppDB?
if (!ready)
val ctx = contextRef.get() ?: return null
// Android 使用 AndroidSqliteDriver
dbSetup(AndroidSqliteDriver(Schema, ctx, name = DB_NAME))
return dbRef
其次是 iOS 实现:
// DBManager.kt (iOS)
package com.example.mykmmapp
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
import kotlin.native.concurrent.AtomicReference
import kotlin.native.concurrent.freeze
actual object DBManager
// 考虑到 iOS 并发的特殊性,为保证多线程共享正确,这里需要使用到 AtomicReference
private val driverRef = AtomicReference<SqlDriver?>(null)
private val dbRef = AtomicReference<MyKmmAppDB?>(null)
private fun dbSetup(driver: SqlDriver)
val db = MyKmmAppDB(driver)
// 初始化后,即刻 freeze
driverRef.value = driver.freeze()
dbRef.value = db.freeze()
fun dbClear()
driverRef.value?.close()
dbRef.value = null
driverRef.value = null
// OC、Swift 调用该方法进行初始化
fun defaultDriver()
dbSetup(NativeSqliteDriver(Schema, DB_NAME))
actual fun getInstance(): MyKmmAppDB?
return dbRef.value
在 Android 工程中添加初始化代码
package com.example.mykmmapp.android
import android.app.Application
import com.example.mykmmapp.DBManager
import java.lang.ref.WeakReference
class App : Application()
override fun onCreate()
super.onCreate()
DBManager.contextRef = WeakReference(this)
在 iOS 工程中添加初始化代码
import UIKit
import shared
@main
class AppDelegate: UIResponder, UIApplicationDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
DBManager.init().defaultDriver()
return true
// ...
Objective-C 示例:
#import "Test.h"
#import <shared/shared.h>
@implementation Test
- (void)dbSetup
[[SharedDBManager init] defaultDriver];
@end
使用 Query
插入数据
进行完初始化操作以后,我们可以在 KMM 模块中调用 sq 文件中定义好的 Query 方法进行数据库的 CRUD 操作,如下面代码所示:
fun insertData()
DBManager.getInstance()?.userQueries?.insertUser(
"张三",
30,
1,
"13800138000"
)
建议不要在双端的 App 代码中直接调用数据库的能力,而是应该将一系列操作放在 KMM 模块中执行
此时在 App 中调用 insertData()
方法进行测试
class MainActivity : AppCompatActivity()
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// ...
insertData()
class ViewController: UIViewController
override func viewDidLoad()
super.viewDidLoad()
// ...
DBTestKt.insertData()
运行 App 以后,我们可以在 Database Inspector 中看到数据库中已经有了刚才插入的数据(iOS 数据库可使用 SQLPro for SQLite 等 App 进行查看)
![](https://img.coderyuan.com/1626071074045.png)
在 App 沙盒目录下,也有了相应的数据库文件
![](https://img.coderyuan.com/1626071242610.jpg)
![](https://img.coderyuan.com/1626071503082.png)
查询数据
首先定义好测试方法,例如按照用户 ID 来查询用户信息,并在控制台打印出来:
fun fetchDBData()
val user = DBManager.getInstance()?.userQueries?.queryById(1)?.executeAsOneOrNull() ?: return
println("User: $user.name is $user.age years old, gender: $if (user.gender == 1L) "M" else "F", phone: $user.phoneNumber")
然后同样在 App 中调用该方法,双端运行效果如下:
![](https://img.coderyuan.com/1626072093771.png)
![](https://img.coderyuan.com/1626072117544.png)
除此之外,SQLDelight 根据 sq 文件生成的查询方法,还可以设置映射,直接转换为 Model 类,或以 List 的形式接收结果
另外,还可以调用 addListener
来监听结果集的变化
数据库迁移/升级
SQLDelight 支持对数据库表结构进行升级,或进行数据迁移,与 Android、iOS 端处理数据库升级的方法类似,其底层也是依赖数据库 Schema 的 Version 变化,在每次 App 初始化数据库时,运行相应的 SQL 语句对表结构进行修改,或迁移现有数据
如果使用 KMM 的 App 需要进行数据库升级,利用 SQLDelight 可以轻松实现,只需要合理利用 SQL 语句,编写 sqm
文件即可
sqm 文件内容
sqm 文件一般会存放一些对表结构进行修改的 SQL 语句,也可以支持在某个表中进行数据填充
-- 1.sqm
ALTER TABLE User ADD COLUMN company TEXT;
-- 2.sqm
ALTER TABLE User ADD COLUMN level INTEGER;
CREATE TABLE Content(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -- 内容 ID
detail TEXT -- 内容详情
);
具体内容,根据业务需要定制即可,但表名要与现有表一致,其他遵循 SQLite 语法即可
另外,由于 sq 文件主要是在 App 新安装时创建数据库表结构,所以 sq 文件中的表结构也要与 sqm 的修改保持一致,sqm 中修改了什么,sq 的声明也应当增加定义,以保证数据库可以正常映射、运行
sqm 文件的命名规则
需要升级的版本号为文件名,如:1.sqm、2.sqm,如果需要从版本 1 升级,则命名为:1.sqm,如果需要从版本 x,则命名为:x.sqm……
sqm 文件也需要和 sq 文件一样,存放在 commonMain 目录中的 sqldelight 子目录中
修改生效
完成 SQL 的编写后,执行一次 Build 或 ./gradlew assembleDebug,SQLDelight 插件会根据 sqm 文件的命名,自动确定 Schema 的版本号,并自动生成 migrate 所需要的 SQL 语句
![](https://img.coderyuan.com/1626078034431.png)
此时重新编译运行 App(原先手机上的 App 不要卸载),并在 Database Inspector 中观察表结构的变化,即可看到数据库中已经新增了 Content 表,User 表也新加入了 company 和 level 字段
![](https://img.coderyuan.com/1626078232359.png)
其他使用建议及注意事项
- 由于数据库读写是 IO 操作,建议统一放在串行异步队列中执行,减少对 UI 线程的阻塞,并保证逻辑正确
- 数据库操作尽量放在 KMM 的 commonMain 目录中,并封装一些工具方法进行调用,保障双端逻辑一致
- SQL 语句中控制好字段的可空类型,在 Kotlin 代码中谨慎处理可空判断
以上是关于KMM 入门使用 SQLDelight 操作数据库的主要内容,如果未能解决你的问题,请参考以下文章
在 KMM (prod) 中使用 SQLDelight 有啥限制
我应该将我的包名用于 KMM SqlDelight 配置吗?
KMM: sqldelight:coroutines-extensions 将 kotlinx-coroutines-core 版本设置为 1.3.9