news 2026/10/8 7:46:01

SQLDelight Kotlin/JS 快速上手:用 Web Worker 驱动在浏览器中异步运行 SQLite

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQLDelight Kotlin/JS 快速上手:用 Web Worker 驱动在浏览器中异步运行 SQLite
  • 后端
  • ORM

【免费下载链接】sqldelight

SQLDelight - Generates typesafe Kotlin APIs from SQL

项目地址:https://gitcode.com/gh_mirrors/sq/sqldelight
点击查看免费下载

本文基于 docs/js_sqlite/index.md 整理撰写,并结合仓库中drivers/web-worker-driver源码与sample-web示例项目进行纵深补充。

导读

SQLDelight 不仅支持 Android、JVM 与 Native 平台,也支持 Kotlin/JS 浏览器目标:通过web-worker-driver,SQLDelight 可以与运行在 [Web Worker] 中的 SQL 实现(如 SQL.js)通信,让所有数据库操作在后台线程中异步执行,避免阻塞浏览器主线程。读完本文,你将掌握在 Kotlin/JS 项目中启用generateAsync、配置 Gradle 依赖、创建 Web Worker 驱动、编写类型安全查询,以及理解驱动与 Worker 之间标准化消息协议的全过程。

!!! info SQLDelight 2.0 之前基于同步实现的sqljs-driver已被异步的web-worker-driver取代。启用该驱动时,必须在 Gradle 配置中设置generateAsync = true。

核心概念:为什么数据库操作要放进 Web Worker

web-worker-driver的设计目标是把所有 SQL 操作从浏览器主线程中剥离出去。它允许 SQLDelight 与一个运行在 [Web Worker] 中的 SQL 实现通信——Worker 是浏览器提供的一种可以在后台线程中执行脚本的机制。这样一来,查询、事务等重活都发生在后台进程,主线程只负责收发消息,UI 不会因为 SQL 执行而卡顿。

该驱动本身是**方言无关(dialect-agnostic)**的:它并不绑定某个具体的 SQL 引擎,而是通过一套标准化消息与 Worker 脚本通信,由 Worker 端负责实际解析和执行 SQL 并回传结果。这一点可以从 WebWorkerDriver.kt 的类注释中得到印证:

A [SqlDriver] implementation for interacting with SQL databases running in a Web Worker. This driver is dialect-agnostic and is instead dependent on the Worker script's implementation to handle queries and send results back from the Worker.

!!! infoweb-worker-driver仅兼容浏览器(browser)目标,不适用于 Node.js 等非浏览器环境。

SQLDelight 官方附带了一个基于 [SQL.js] 的 Worker 实现(sqljs.worker.js),你也可以按照消息协议实现自己的 Worker。下文先介绍最快捷的官方路径,再深入协议细节。

第一步:配置 Gradle 与生成异步数据库代码

1.1 应用 SQLDelight 插件并设置generateAsync

在 Kotlin/JS 工程中,首先在build.gradle.kts(或build.gradle)中应用 SQLDelight Gradle 插件,并注册数据库。关键一步是开启generateAsync,让 SQLDelight 为异步驱动生成对应的挂起 API(例如awaitAsList()等扩展):

=== "Kotlin DSL" ```kotlin plugins { id("app.cash.sqldelight") version "2.x.x" }

repositories { google() mavenCentral() } sqldelight { databases { register("Database") { // 生成出的数据库类名 packageName.set("com.example") generateAsync.set(true) } } } ```

=== "Groovy DSL" ```groovy plugins { id "app.cash.sqldelight" version "2.x.x" }

repositories { google() mavenCentral() } sqldelight { databases { register("Database") { // 生成出的数据库类名 packageName = "com.example" generateAsync = true } } } ```

其中版本号2.x.x请替换为你实际使用的 SQLDelight 版本(仓库中的版本目录见 gradle/libs.versions.toml,坐标统一为app.cash.sqldelight,见 gradle.properties 中的GROUP定义)。

generateAsync是异步驱动的前提:只有开启它,SQLDelight 才会为生成的Database、Queries等类提供与异步SqlDriver配套的 API 形态,配合挂起扩展使用。

1.2 添加web-worker-driver与 Webpack 插件依赖

在jsMain源码集加入驱动依赖。copy-webpack-plugin用于在构建产物中复制 Worker 脚本,是浏览器打包时的必备配套:

=== "Kotlin DSL"kotlin kotlin { sourceSets.jsMain.dependencies { implementation("app.cash.sqldelight:web-worker-driver:2.x.x") implementation(devNpm("copy-webpack-plugin", "9.1.0")) } }

=== "Groovy DSL"groovy kotlin { sourceSets.jsMain.dependencies { implementation "app.cash.sqldelight:web-worker-driver:2.x.x" implementation devNpm("copy-webpack-plugin", "9.1.0") } }

这里devNpm表示该 npm 包只参与开发/构建期打包,不会打进运行时产物。仓库中sample-web的settings.gradle(见 sample-web/settings.gradle)展示了web-worker-driver如何通过includeBuild与dependencySubstitution被示例工程引用,可作为多工程引用驱动时的参考。

第二步:配置一个具体的 Web Worker

2.1 引入 SQL.js 及官方 Worker 包

SQLDelight 提供的官方 Worker 实现基于 SQL.js(一个编译为 WebAssembly 的 SQLite)。先在jsMain中同时添加 worker 包与 SQL.js 的 npm 依赖:

=== "Kotlin DSL"kotlin kotlin { sourceSets.jsMain.dependencies { implementation(npm("@cashapp/sqldelight-sqljs-worker", "2.x.x")) implementation(npm("sql.js", "1.8.0")) } }

=== "Groovy DSL"groovy kotlin { sourceSets.jsMain.dependencies { implementation npm("@cashapp/sqldelight-sqljs-worker", "2.x.x") implementation npm("sql.js", "1.8.0") } }

详细说明参见仓库文档 docs/js_sqlite/sqljs_worker.md。

2.2 用 Webpack 配置复制 WASM 二进制

SQL.js 包含一个 WebAssembly 二进制文件(sql-wasm.wasm),必须把它复制到应用构建输出中。为此在工程根目录添加一个额外的 Webpack 配置文件(Kotlin/JS 会自动加载webpack.config.d/下的.js文件),例如:

// {project}/webpack.config.d/sqljs.js config.resolve = { fallback: { fs: false, path: false, crypto: false, } }; const CopyWebpackPlugin = require('copy-webpack-plugin'); config.plugins.push( new CopyWebpackPlugin({ patterns: [ '../../node_modules/sql.js/dist/sql-wasm.wasm' ] }) );

这段配置在仓库中真实存在,见 sample-web/webpack.config.d/sqljs-config.js。其中resolve.fallback将 Node 内置模块fs、path、crypto关掉,避免浏览器打包时报错;CopyWebpackPlugin负责把sql.js的 WASM 文件复制到产物目录。

!!! noteweb-worker-driver模块内的webpack.config.d/fs.js(见 drivers/web-worker-driver/webpack.config.d/fs.js)同样对fs等 Node 模块做了 fallback 处理,这是浏览器端打包 WebAssembly 驱动时的一项通用要求。

2.3 测试时的 Karma 配置

运行浏览器测试时,还需在工程的karma.config.d/目录下添加 Karma 配置,让测试运行时能定位到 WASM 二进制。仓库中的 sample-web/karma.config.d/sqljs-config.js 给出了完整参考,核心思路是:

  1. 把sql.js/dist/sql-wasm.wasm作为静态文件供 Karma 服务;
  2. 通过config.proxies["/sql-wasm.wasm"]建立 URL 代理;
  3. 为 webpack 指定一个临时输出目录,并把输出内容也加入 Karma 的files列表(这是为了让 webpack 动态产物能被 Karma 识别,参考自 karma-webpack 的已知问题处理方案)。
const path = require("path"); const os = require("os"); const dist = path.resolve("../../node_modules/sql.js/dist/") const wasm = path.join(dist, "sql-wasm.wasm") config.files.push({ pattern: wasm, served: true, watched: false, included: false, nocache: false, }); config.proxies["/sql-wasm.wasm"] = path.join("/absolute/", wasm) const output = { path: path.join(os.tmpdir(), '_karma_webpack_') + Math.floor(Math.random() * 1000000), } config.set({ webpack: {...config.webpack, output} }); config.files.push({ pattern: `${output.path}/**/*`, watched: false, included: false, });

第三步:在代码中创建 Web Worker 驱动

3.1 通过Worker+URL引用 Worker 脚本

创建WebWorkerDriver时,必须传入一个指向 Worker 脚本的Worker实例。Worker构造函数接受一个URL对象:

val driver = WebWorkerDriver( Worker( js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)""") ) )

这里的关键是 Webpack 对import.meta.url的特殊支持:当URL的第二个参数是import.meta.url时,Webpack 会在构建期自动解析并打包来自 npm 包(@cashapp/sqldelight-sqljs-worker)的 Worker 脚本。

!!! warning 为了让 Webpack 正确解析这个 URL,必须在js()代码块中完整构造URL对象(如上所示),且必须带上import.meta.url参数。不要在js()外部拼接或拆开构造,否则 Webpack 无法识别该 Worker 引用。

该写法在仓库中有多处真实佐证:JS 平台的默认工厂函数 CreateDefaultWebWorkerDriver.kt 中createDefaultWebWorkerDriver()的actual实现,以及示例工程 sample-web/src/jsMain/kotlin/com/example/sqldelight/hockey/data/DbHelper.kt 中DbHelper的初始化代码,都采用完全相同的new URL(..., import.meta.url)模式。

从commonMain的 CreateWebWorkerDriver.kt 可以看到,createDefaultWebWorkerDriver()是一个expect函数,返回SqlDriver,各平台各自提供actual实现——JS 平台默认就指向官方 SQL.js Worker。

3.2 像普通驱动一样使用

创建好驱动后,WebWorkerDriver实现了 SQLDelight 的SqlDriver接口,因此可以无缝用于生成的Database类及其他 SQLDelight API。它内部通过WorkerWrapper包装真实 Worker 进行消息收发:

  • 查询与执行:executeQuery/execute都会把 SQL 与绑定参数封装成请求消息发送给 Worker(见 WebWorkerDriver.kt);
  • 事务:newTransaction()会向 Worker 发送begin_transaction;endTransaction(successful)在成功时发送end_transaction、失败时发送rollback_transaction,且支持嵌套事务(见 WebWorkerDriver.kt);
  • 关闭:close()最终调用wrapper.terminate()终止 Worker。

每次发送消息时,驱动内部维护一个自增的messageCounter作为消息id,用于后续匹配 Worker 的响应(见 WebWorkerDriver.kt 与 WorkerWrapperRequest.kt)。

第四步:定义并使用类型安全查询

4.1 在.sq文件中编写带标签的 SQL

SQLDelight 会为.sq文件中任何带标签的 SQL 语句生成类型安全函数。例如src/main/sqldelight/com/example/sqldelight/hockey/data/Player.sq:

selectAll: SELECT * FROM hockeyPlayer; insert: INSERT INTO hockeyPlayer(player_number, full_name) VALUES (?, ?); insertFullPlayerObject: INSERT INTO hockeyPlayer(player_number, full_name) VALUES ?;

对于每条带标签语句,SQLDelight 会生成一个对应的类型安全函数,参数、返回类型均由 SQL 推断而来。仓库中真实的示例见 sample-web/src/jsMain/sqldelight/com/example/sqldelight/hockey/data/Player.sq——其中selectAll、insertPlayer、forTeam等标签语句展示了 JOIN、命名参数(:team_id)与 CAST 的用法。

4.2 通过生成的 Queries 对象调用

每个包含标签语句的.sq文件会生成一个 "Queries" 对象,例如Player.sq生成PlayerQueries:

suspend fun doDatabaseThings(driver: SqlDriver) { val database = Database(driver) val playerQueries: PlayerQueries = database.playerQueries println(playerQueries.selectAll().awaitAsList()) // [HockeyPlayer(15, "Ryan Getzlaf")] playerQueries.insert(player_number = 10, full_name = "Corey Perry") println(playerQueries.selectAll().awaitAsList()) // [HockeyPlayer(15, "Ryan Getzlaf"), HockeyPlayer(10, "Corey Perry")] val player = HockeyPlayer(10, "Ronald McDonald") playerQueries.insertFullPlayerObject(player) }

!!! warning 使用异步驱动时,运行查询请使用挂起的awaitAs*()扩展函数(如awaitAsList()、awaitAsOne()、awaitAsOneOrNull()),不要使用阻塞式的executeAs*()函数。这些扩展定义在 extensions/async-extensions/src/commonMain/kotlin/app/cash/sqldelight/async/coroutines/QueryExtensions.kt 中,此外 DriverExtensions.kt 还提供awaitCreate()、await()等挂起封装,用于异步创建表结构(Schema)。

Database类与关联的Schema对象由generateSqlDelightInterfaceGradle 任务生成;该任务会在你编辑.sq文件时由 SQLDelight IDE 插件自动运行,也会在常规 Gradle 构建中自动执行(见 docs/common/index_schema.md)。

深入原理:驱动与 Worker 的标准化消息协议

web-worker-driver之所以能对接任意 SQL 实现,是因为它定义了一套与方言、实现无关的消息格式。每个从驱动发往 Worker 的消息都包含一个action属性,指明四类动作之一(对应 WorkerAction.kt 中WorkerActions的四个常量)。完整协议细节参见 docs/js_sqlite/custom_worker.md。

exec

指示 Worker 执行消息中附带的 SQL 语句并返回查询结果。消息携带sql(要执行的 SQL)与params(要绑定的参数数组):

{ "id": 5, "action": "exec", "sql": "SELECT column_a, column_b FROM some_table WHERE column_a = ?;", "params": ["value"] }

begin_transaction

通知 Worker 开始一个事务:

{ "id": 2, "action": "begin_transaction" }

end_transaction

通知 Worker 结束(提交)当前事务:

{ "id": 3, "action": "end_transaction" }

rollback_transaction

通知 Worker 回滚当前事务:

{ "id": 8, "action": "rollback_transaction" }

响应格式:id与results

每条入站消息都带有一个唯一的整数id,Worker 在响应中必须原样带回该id,驱动据此把响应匹配到对应的请求(仓库中由 WorkerWrapperRequest.kt 的id字段与 WebWorkerDriver.kt 的messageCounter机制实现)。

响应还包含results属性,用于承载 SQL 执行结果:

  • 对于查询语句,results是一个数组,代表结果集的行;其中每一项又是一个数组,代表该行的列。例如上面exec消息的响应可以是:
{ "id": 5, "results": [ ["value", "this is the content of column_b"], ["value", "this is a different row"] ] }
  • 对于不返回结果集的语句(如 INSERT/UPDATE),results应包含单个行/列,其数值代表受影响的行数:
{ "id": 10, "results": [ [1] ] }

官方 SQL.js Worker 是如何实现的

SQLDelight 官方提供的 Worker 脚本位于 drivers/web-worker-driver/sqljs/sqljs.worker.js,它完整实现了上述协议,可作为自定义 Worker 的范本:

  • 通过importScripts检测是否处于 Worker 环境,用initSqlJs({ locateFile: file => '/sql-wasm.wasm' })加载 WASM 版 SQLite,并创建db = new SQL.Database();
  • 在self.onmessage中按action分发:exec用db.exec(data.sql, data.params)执行并回传结果;begin_transaction/end_transaction/rollback_transaction分别执行BEGIN TRANSACTION;/END TRANSACTION;/ROLLBACK TRANSACTION;;
  • 执行出错时通过onError回传{ id: data.id, error: err }。

完整示例:仓库中的sample-web项目

仓库中的sample-web是一个完整的 Kotlin/JS + Web Worker 参考实现(目录见 sample-web/),包含了上述所有环节的落地代码:

  • 驱动创建:DbHelper在init块中用WebWorkerDriver(Worker(js("""new URL(...)")))创建驱动,并用Mutex保证并发安全(见 DbHelper.kt);
  • 建表与种子数据:通过HockeyDb.Schema.awaitCreate(driver)异步建表,随后用生成的teamQueries/playerQueries插入球队与球员数据,还演示了EnumColumnAdapter、IntColumnAdapter、FloatColumnAdapter等自定义列适配器的用法;
  • 查询渲染:Main.kt在协程中用playerQueries.forTeam(-1).awaitAsList()与teamQueries.selectAll().awaitAsList()取数,并渲染成 HTML 表格(见 Main.kt);
  • 测试:HockeyDbTest用runTest+DbHelper跑浏览器测试,验证teamsCreated、playersCreated(见 HockeyDbTest.kt)。

注意事项与限制

  • 仅限浏览器目标:web-worker-driver依赖浏览器 Web Worker API,只适用于js的浏览器运行环境;
  • 必须开启generateAsync:忘记设置会导致生成的 API 与异步驱动不匹配,这是从旧版sqljs-driver迁移到web-worker-driver时最常见的坑;
  • URL构造必须完整写在js()块内:否则 Webpack 无法在构建期解析并打包 Worker 脚本;
  • WASM 文件必须随产物发布:生产构建靠copy-webpack-plugin,测试靠 Karma 配置,二者缺一不可;
  • 查询要使用挂起扩展:异步驱动下请使用awaitAsList()/awaitAsOne()/awaitAsOneOrNull(),不要使用阻塞式executeAs*()。

从web-worker-driver的消息协议出发,你还可以参照 docs/js_sqlite/custom_worker.md 实现自己的 Worker(例如接入其他 SQL 引擎或自定义加密存储),SQLDelight 驱动端无需任何改动——这正是该驱动"方言无关、协议标准化"设计带来的扩展价值。

  • 后端
  • ORM

【免费下载链接】sqldelight

SQLDelight - Generates typesafe Kotlin APIs from SQL

项目地址:https://gitcode.com/gh_mirrors/sq/sqldelight
点击查看免费下载
上一篇:终极解决方案:Minecraft Photon着色器运动模糊加载失败深度排查与修复指南
下一篇:ComfyUI-BrushNet 图像掩码编辑功能解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 7:45:17

对话式AI的上下文管理:Context-Mode模式设计与工程实践

接手过不少对话式 AI 项目之后,你会发现一个很现实的问题:模型能力本身进步很快,但真正让应用“好用”的,往往不是模型,而是你怎样管理它面前的那一摞“历史记录”。这个“历史记录”就是上下文。所谓 context-mode&am…

作者头像 李华
网站建设 2026/10/8 7:43:22

OpenCV DNN C++实战:灰度图上色与饱和度参数调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华