Kotlin 跨平台 SqliteNow 全平台数据持久化方案
Kotlin 跨平台 SqliteNow 全平台数据持久化方案
- 1. 环境与依赖配置
- 1.0 创建一个Kotlin 多平台项目
- 1.1 版本声明(libs.versions.toml)
- 1.2 项目级插件配置(build.gradle.kts)
- 1.3 模块级依赖配置(app/shared/build.gradle.kts)
- 1.4 Web 平台配置(Webpack)
- 1.5 WasmJs 平台资源复制配置
- 2 创建数据库
- 2.0 添加数据库构建脚本
- 2.1 添加学生的Kotlin类
- 2.2 添加学生表和基础查询语句
- 2.3 测试你的数据库
- 3. 测试数据库
- 3.0 在JVM平台测试数据库
- 3.1 在Web Js平台测试数据库
- 3.2 在Web Wasm平台测试数据库
- 3.3 Web平台常见错误修复
SqliteNow 是一款面向 Kotlin Multiplatform(KMP)的全平台 SQLite 持久化库,它通过编译时 SQL 验证、类型安全的查询构建器以及跨平台(Android、iOS、Desktop、Web)的统一 API,为 KMP 项目提供了高效、可靠的数据存储解决方案。本文将以一个学生信息管理为例,详细介绍如何从零开始集成 SqliteNow,完成表结构定义、数据操作,并在 JVM(Desktop)与 Web(JS/Wasm)平台上验证数据持久化效果。
项目源码:
本文涉及的多平台库源地址可在 GitHub - sqlitenow-kmp 查看。
详情教程可在 GitHub sqlitenow-kmp wiki 查看。
1. 环境与依赖配置
1.0 创建一个Kotlin 多平台项目
在IntelliJ IDEA 2026.1.2中创建一个Kotlin多平台项目, 项目目录如下:
1.1 版本声明(libs.versions.toml)
首先在项目的gradle/libs.versions.toml中声明所需库的版本:
[versions] android-minSdk = "26" # sqlitenow 需要最低SDK版本 26 kotlin = "2.3.21" sqlite = "2.6.2" sqlitenow = "0.9.1" kotlinx-datetime = "0.8.0" kotlinx-serialization-json = "1.11.0" [libraries] sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } sqlitenow-core = { module = "dev.goquick.sqlitenow:core", version.ref = "sqlitenow" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } [plugins] sqlitenow = { id = "dev.goquick.sqlitenow", version.ref = "sqlitenow" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }1.2 项目级插件配置(build.gradle.kts)
在项目根目录的build.gradle.kts中声明插件(apply false 表示不在根项目直接应用):
plugins{alias(libs.plugins.sqlitenow)applyfalsealias(libs.plugins.kotlinSerialization)applyfalse}1.3 模块级依赖配置(app/shared/build.gradle.kts)
在共享模块app/shared的build.gradle.kts中应用插件并配置平台特定依赖, 然后同步项目:
plugins{alias(libs.plugins.sqlitenow)alias(libs.plugins.kotlinSerialization)}kotlin{sourceSets{androidMain.dependencies{implementation(libs.sqlite.bundled)}iosMain.dependencies{implementation(libs.sqlite.bundled)}jvmMain.dependencies{implementation(libs.sqlite.bundled)}commonMain.dependencies{implementation(libs.sqlitenow.core)implementation(libs.kotlinx.datetime)implementation(libs.kotlinx.serialization.json)}}}1.4 Web 平台配置(Webpack)
对于 Web 平台(JS 目标),需要在app/webApp模块中配置 Webpack,以正确处理 SQL.js 的 WASM 资源。创建webpack.config.d/sqljs.js:
config.resolve={fallback:{fs:false,path:false,crypto:false,}};config.module=config.module||{};config.module.rules=Array.isArray(config.module.rules)?config.module.rules:[];config.module.rules.push({test:/sql-wasm\.wasm$/,type:"asset/resource",});constCopyWebpackPlugin=require('copy-webpack-plugin');config.plugins.push(newCopyWebpackPlugin({patterns:['../../node_modules/sql.js/dist/sql-wasm.wasm']}));1.5 WasmJs 平台资源复制配置
对于 WebAssembly(WasmJs)目标,需在共享模块的构建脚本中添加资源复制任务,确保 WASM 文件能被正确打包。在app/shared/build.gradle.kts的kotlin块后添加:
kotlin{sourceSets{webMain.dependencies{implementation(devNpm("copy-webpack-plugin","11.0.0"))implementation(npm("sql.js","1.13.0"))}}}tasks.named<ProcessResources>("wasmJsProcessResources"){valrootBuildDir=rootProject.layout.buildDirectoryvalsqlitenowPath="wasm/node_modules/sqlitenow-kmp-library-core"valmoveFileList=listOf("sql-wasm.wasm","sqlitenow-sqljs.js","sqlitenow-indexeddb.js")moveFileList.forEach{fileName->from(rootBuildDir.file("$sqlitenowPath/$fileName"))}duplicatesStrategy=DuplicatesStrategy.INCLUDE}构建完成后检查app/shared/build/processedResources/wasmJs/main中是否包含以下三个文件sql-wasm.wasm,sqlitenow-sqljs.js,sqlitenow-indexeddb.js
2 创建数据库
2.0 添加数据库构建脚本
在app/shared模块级别build.gradle.kts的中添加数据库构建脚本
sqliteNow{databases{create("AppDatabase"){packageName.set("com.example.sqliteNow")debug=false}}}2.1 添加学生的Kotlin类
在app/shared/commonMain中创建sql目录, 然后在该目录下创建AppDatabase目录(与构建脚本中名称相同),最后在AppDatabase目录下, 创建schema和queries目录
在app/shared/commonMain中创建Student.kt文件, 表示数据库中的学生实例
packageorg.example.kmpsqlitenowdemoimportkotlinx.datetime.LocalDateimportkotlinx.serialization.Serializable@SerializableenumclassGender{MALE,FEMALE}dataclassStudent(valid:Long=0,valname:String,valage:Int,valgender:Gender,valbirthDate:LocalDate)2.2 添加学生表和基础查询语句
在schema目录中, 创建一个学生表student.sql
CREATETABLEIFNOTEXISTSstudent(idINTEGERPRIMARYKEYNOTNULL,nameTEXTNOTNULL,ageINTEGERNOTNULL,-- @@{field=gender, propertyType=org.example.kmpsqlitenowdemo.Gender, adapter=custom}genderTEXTNOTNULL,-- @@{field=birthDate, propertyType=kotlinx.datetime.LocalDate, adapter=custom}birthDateTEXTNOTNULL);在queries目录中创建student目录,然后创建一个查询全部语句selectAll.sql
-- @@{ queryResult=StudentEntity }SELECT*FROMstudent;在queries/student目录中创建一个添加学生的语句add.sql
INSERTINTOstudent(name,age,gender,birthDate)VALUES(:name,:age,:gender,:birthDate);最后同步项目,运行 Gradle 任务生成数据库代码:./gradlew :app:shared:generateAppDatabase
2.3 测试你的数据库
在app/shared/commonMain中创建testDatabase.kt文件, 用于测试数据库, 如果没有学生George, 那么会添加, 然后输出学生表
packageorg.example.kmpsqlitenowdemoimportcom.example.sqliteNow.AppDatabaseimportcom.example.sqliteNow.StudentQueryimportcom.example.sqliteNow.VersionBasedDatabaseMigrationsimportdev.goquick.sqlitenow.common.resolveDatabasePathimportdev.goquick.sqlitenow.core.util.fromSqliteDateimportdev.goquick.sqlitenow.core.util.toSqliteDateimportkotlinx.coroutines.delayimportkotlinx.datetime.LocalDateimportkotlinx.serialization.json.Jsonimportkotlin.time.Duration.Companion.millisecondssuspendfuntestDatabase(){valdb=AppDatabase(dbName=resolveDatabasePath(dbName="database.db",appName="KmpSqliteNowDemo"),migration=VersionBasedDatabaseMigrations(),studentAdapters=AppDatabase.StudentAdapters(genderToSqlValue={gender->Json.encodeToString(gender)},sqlValueToGender={string->Json.decodeFromString(string)},birthDateToSqlValue={localDate->localDate.toSqliteDate()},sqlValueToBirthDate={string->LocalDate.fromSqliteDate(string)}))db.open()println("[Database] opened")valstudentList=db.student.selectAll.asList()println("[Database] studentList:$studentList")if(studentList.find{it.name=="George"}==null){valparams=StudentQuery.Add.Params(name="George",age=19,gender=Gender.MALE,birthDate=LocalDate(year=2003,month=7,day=27))db.student.add(params)println("[Database] add George successful")}valnowStudentList=db.student.selectAll.asList()println("[Database] nowStudentList:$nowStudentList")delay(1000.milliseconds)db.close()println("[Database] closed")}在app/shared/commonMain中App.kt文件的App函数下, 添加下面代码, 用于测试数据库
funApp(){LaunchedEffect(Unit){testDatabase()}// ...}3. 测试数据库
3.0 在JVM平台测试数据库
然后先在desktop平台运行一下, 会产生以下输出
[Database] opened [Database] studentList: [] [Database] add George successful [Database] nowStudentList: [StudentEntity(id=1, name=George, age=19, gender=MALE, birthDate=2003-07-27)] [Database] closed重新运行输出结果如下, 说明学生George成功添加到了数据库中, 完成了持久化
[Database] opened [Database] studentList: [StudentEntity(id=1, name=George, age=19, gender=MALE, birthDate=2003-07-27)] [Database] nowStudentList: [StudentEntity(id=1, name=George, age=19, gender=MALE, birthDate=2003-07-27)] [Database] closed3.1 在Web Js平台测试数据库
在web Js平台也运行一下
[SqliteNow][OPFS] Using Origin Private File System persistence for database.db [SqliteNow] No persisted snapshot for database.db [SqlJs] loading sql.js module… [SqlJs] sql.js module loaded [SqlJs] Opening database.db with empty in-memory database [Database] opened [Database] studentList: [] [Database] add George successful [Database] nowStudentList: [StudentEntity(id=1, name=George, age=19, gender=MALE, birthDate=2003-07-27)] [Database] closed重新运行结果如下
[SqliteNow][OPFS] Using Origin Private File System persistence for database.db [SqliteNow] Restored 8192 bytes for database.db [SqlJs] loading sql.js module… [SqlJs] sql.js module loaded [SqlJs] Opening database.db from persisted snapshot (8192 bytes) [Database] opened [Database] studentList: [StudentEntity(id=1, name=George, age=19, gender=MALE, birthDate=2003-07-27)] [Database] nowStudentList: [StudentEntity(id=1, name=George, age=19, gender=MALE, birthDate=2003-07-27)] [Database] closed说明学生George成功添加到了数据库中, 完成了持久化
3.2 在Web Wasm平台测试数据库
在web WasmJs平台也运行一下
(sqlitenow) [sqlitenow] [SqliteNow][IndexedDB] Using IndexedDB persistence for database.db [SqliteNow] No persisted snapshot for database.db (sqlitenow) [sqlitenow] [SqlJs][Wasm] Loading sql.js module… (sqlitenow) [sqlitenow] [SqlJs][Wasm] sql.js module ready (sqlitenow) [sqlitenow] [SqlJs][Wasm] Kotlin opening database.db without snapshot (new database) [Database] opened [Database] studentList: [] [Database] add George successful [Database] nowStudentList: [StudentEntity(id=1, name=George, age=19, gender=MALE, birthDate=2003-07-27)] [Database] closed重新运行结果如下
(sqlitenow) [sqlitenow] [SqliteNow][IndexedDB] Using IndexedDB persistence for database.db [SqliteNow] Restored 8192 bytes for database.db (sqlitenow) [sqlitenow] [SqlJs][Wasm] Loading sql.js module… (sqlitenow) [sqlitenow] [SqlJs][Wasm] sql.js module ready (sqlitenow) [sqlitenow] [SqlJs][Wasm] Kotlin opening database.db with snapshot bytes=8192 [Database] opened [Database] studentList: [StudentEntity(id=1, name=George, age=19, gender=MALE, birthDate=2003-07-27)] [Database] nowStudentList: [StudentEntity(id=1, name=George, age=19, gender=MALE, birthDate=2003-07-27)] [Database] closed说明学生George成功添加到了数据库中, 完成了持久化
3.3 Web平台常见错误修复
如果在web平台中出现,打开数据库后找不到student表的错误,或者表格字段错误等等,请在浏览器设置中,清除浏览数据在尝试,这将清除你在开发环境浏览器中保留的IndexedDB
如果Web Wasm平台出现下面错误,请运行位于web平台中的 Gradle 自定义任务
tasks.named<ProcessResources>("wasmJsProcessResources"){valrootBuild=rootProject.layout.buildDirectoryvalsqlitenowPath="wasm/node_modules/sqlitenow-kmp-library-core"from(rootBuild.file("$sqlitenowPath/sqlitenow-sqljs.js"),rootBuild.file("$sqlitenowPath/sql-wasm.wasm"),rootBuild.file("$sqlitenowPath/sqlitenow-indexeddb.js"))duplicatesStrategy=DuplicatesStrategy.INCLUDE}错误截图如下
