- 后端
- ORM
【免费下载链接】sqldelight
SQLDelight - Generates typesafe Kotlin APIs from SQL
当你的数据库以.sqm迁移文件为 Schema 的唯一事实来源,且希望 Flyway 这类外部服务直接读取迁移脚本时,SQLDelight 的迁移文件会因为包含 Kotlin 类型声明而不再是合法 SQL。本文围绕 docs/jvm_postgresql/migrations.md 的核心主题,完整讲解如何通过migrationOutputDirectory与migrationOutputFileFormat两个 Gradle 配置项,让 SQLDelight 生成一份可供外部服务消费的"合法 SQL 迁移产物",并结合仓库中的任务实现源码与集成测试,说明其背后的工作原理、任务命名规则与接入方式。
为什么.sqm迁移文件会"失去合法 SQL 身份"
在 SQLDelight 中,Schema 有两种来源方式(详见 docs/common/index_server.md):
- Fresh Schema:在
.sq文件中直接写CREATE TABLE等语句,一次性从空库建出目标 Schema; - Migration Schema:在
.sqm迁移文件中按顺序书写CREATE/ALTER语句,Schema 由迁移逐步叠加而成。
Migration Schema 场景下,迁移文件允许使用自定义 Kotlin 类型。例如在 docs/common/types_server_migrations.md 中可以看到,ALTER TABLE时可以直接为列声明暴露给 Kotlin 的类型:
import kotlin.String; import kotlin.collection.List; ALTER TABLE my_table ADD COLUMN new_column VARCHAR(8) AS List<String>;仓库测试夹具 1.sqm 也给出了同样的用法:
import kotlin.collections.List; CREATE TABLE person ( id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL, friends TEXT AS List<String> );问题就在这里:import ...;与AS List<String>这类语法是 SQLDelight 的扩展,标准 SQL 引擎并不认识它们。因此,一旦迁移文件里出现了自定义 Kotlin 类型,整个.sqm文件就不再是合法 SQL,无法直接交给 PostgreSQL 服务器或 Flyway 等外部迁移工具执行。
方案总览:把迁移导出为"合法 SQL 产物"
为了让其他服务(典型如 Flyway)能读取迁移文件,SQLDelight 提供了一种输出机制:额外生成一份剥离了 SQLDelight 扩展语法、只含标准 SQL 语句的迁移文件副本。这些副本会输出到你指定的目录中,再通过让 Kotlin 编译任务依赖生成任务,使产物进入运行时 Classpath,供外部迁移工具消费。
整个链路可以概括为:
.sqm(含 Kotlin 类型,非合法 SQL) │ generateMainDatabaseMigrations ▼ 输出目录(.sql 文件,标准 SQL,供 Flyway 等服务读取) │ compileKotlin dependsOn ▼ 编译产物 / ClasspathGradle 配置:两个核心配置项
文档给出的 Groovy 配置如下(原文括号略有缺失,此处补齐为完整可用的写法):
sqldelight { databases { Database { migrationOutputDirectory = layout.buildDirectory.dir("resources/main/migrations") migrationOutputFileFormat = ".sql" // 默认为 .sql } } }对应 Kotlin DSL 写法为:
sqldelight { databases { create("Database") { migrationOutputDirectory.set(layout.buildDirectory.dir("resources/main/migrations")) migrationOutputFileFormat.set(".sql") // 默认为 .sql } } }两个配置项在源码 SqlDelightDatabase.kt 中定义如下:
| 配置项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
migrationOutputDirectory | DirectoryProperty | 未设置(未设置时不会注册输出任务) | 合法 SQL 迁移产物的输出目录 |
migrationOutputFileFormat | Property<String> | ".sql" | 输出文件的扩展名格式,需以点号开头 |
注意migrationOutputDirectory的语义:从 SqlDelightDatabase.kt 的实现看,只有当该属性被显式设置(isPresent)时,插件才会注册迁移输出任务;不设置则完全不会生成相关任务。
仓库测试夹具 schema-output/build.gradle 给出了真实项目中的用法,其输出目录为resources/migrations:
sqldelight { databases { MyDatabase { packageName = "app.cash.sqldelight.mysql.integration" dialect("app.cash.sqldelight:mysql-dialect:${app.cash.sqldelight.VersionKt.VERSION}") migrationOutputDirectory = layout.buildDirectory.dir("resources/migrations") migrationOutputFileFormat = ".sql" } } }生成任务:generateMainDatabaseMigrations
配置完成后,插件会注册一个名为generateMainDatabaseMigrations的任务。任务名的构成规则在 SqlDelightDatabase.kt 中一目了然:
generate${Source 名(首字母大写)}${数据库名}Migrations- 默认 Source 名为
main,默认数据库名为Database,组合得到generateMainDatabaseMigrations; - 若数据库命名为
MyDatabase,任务名就是generateMainMyDatabaseMigrations——集成测试 DialectIntegrationTests.kt 正是通过gradle clean generateMainMyDatabaseMigrations来驱动该任务的。
该任务由 GenerateMigrationOutputTask.kt 实现,其行为包含几个值得注意的细节:
- 任务被标记为
@CacheableTask,且outputDirectory声明为@OutputDirectory,migrationOutputExtension声明为@Input,具备 Gradle 增量缓存能力; - 执行前会清空输出目录(源码中
outputDirectory.listFiles()?.forEach { it.delete() }),保证产物与当前迁移文件严格一致,不会残留旧版本文件; - 对每个
.sqm迁移文件逐一输出:文件名取迁移文件主名(去掉.sqm后缀),再拼接migrationOutputFileFormat指定的扩展名; - 输出内容为纯 SQL:逐条取出
sqlStmtList中的语句,调用rawSqlText()取原始 SQL 文本并在末尾追加分号,语句之间以空行分隔。
正是第 3、4 点保证了"合法 SQL"的语义:import声明与AS Kotlin类型这类扩展语法不会被写入输出文件,外部服务拿到的是一份可以被 PostgreSQL 原生解析的标准 SQL 脚本。
接入compileKotlin:让 Flyway 在 Classpath 上找到迁移
文档给出的核心接入方式是让 Kotlin 编译任务依赖迁移输出任务:
compileKotlin.configure { dependsOn "generateMainDatabaseMigrations" }Kotlin DSL 等价写法:
tasks.named("compileKotlin") { dependsOn("generateMainDatabaseMigrations") }这样做的意义在于:迁移产物位于build/resources/main/migrations这类目录下时,其内容会随compileKotlin的执行被纳入运行时资源。对于 Flyway 这类从 Classpath 扫描迁移脚本的服务而言,只要配置了对应的迁移路径,就能读取到这份合法 SQL 产物,无需再手工维护一份"给数据库用的 SQL 副本"。
完整的 PostgreSQL 服务端配置示例
结合 PostgreSQL 服务端场景(数据库deriveSchemaFromMigrations = true、使用postgresql-dialect),一份完整的构建配置可参考仓库集成测试 integration-postgresql-migrations/build.gradle:
plugins { alias(libs.plugins.kotlin.jvm) alias(libs.plugins.sqldelight) } sqldelight { databases { MyDatabase { packageName = "app.cash.sqldelight.postgresql.integration" dialect("app.cash.sqldelight:postgresql-dialect:${app.cash.sqldelight.VersionKt.VERSION}") deriveSchemaFromMigrations = true // 将迁移输出为合法 SQL,供 Flyway 等外部服务使用 migrationOutputDirectory = layout.buildDirectory.dir("resources/main/migrations") migrationOutputFileFormat = ".sql" } } }其中:
deriveSchemaFromMigrations = true表示 Schema 由.sqm迁移文件推导,而不是写在.sq里(默认false,详见 docs/common/gradle.md);- PostgreSQL 方言依赖为
app.cash.sqldelight:postgresql-dialect,与该场景的入口文档 docs/jvm_postgresql/index.md 保持一致; - 迁移文件位于
src/main/sqldelight下(可通过srcDirs调整),文件名必须包含数字以决定执行顺序。
输出结果长什么样:从.sqm到.sql
以 PostgreSQL 集成测试的迁移文件为例,输入 V1__Product.sqm:
CREATE TABLE Products ( id SERIAL PRIMARY KEY, sku VARCHAR(255) NOT NULL ); COMMENT ON TABLE Products IS 'Store the sku rows by id'; COMMENT ON COLUMN Products.sku IS 'The sku is not a unique identifier';以及后续迁移 V2__Product_alter.sqm:
ALTER TABLE Products ADD COLUMN requires_dpi BOOLEAN DEFAULT FALSE; COMMENT ON COLUMN Products.requires_dpi IS 'requires_dpi is false by default';这些文件本身就是合法 SQL,输出任务会按迁移顺序逐条重写为带分号的语句并写入输出目录。而如果某个迁移使用了自定义 Kotlin 类型(例如ALTER TABLE dog ADD COLUMN is_good INTEGER AS kotlin.Boolean DEFAULT TRUE NOT NULL;,见 2.sqm),输出时AS kotlin.Boolean与文件头部的import会被剥离,最终产物只保留ALTER TABLE dog ADD COLUMN is_good INTEGER DEFAULT TRUE NOT NULL;这类标准 SQL。
与迁移校验任务的配合
需要说明的是,迁移输出任务与迁移校验(VerifyMigrationTask)是两个独立的机制:
- 校验:
verifyMainDatabaseMigration任务(聚合任务verifySqlDelightMigration的组成部分)会在check阶段运行,负责验证迁移链与 Schema 的一致性。从 VerifyMigrationTask.kt 的实现可以看到,它会检查迁移文件版本是否连续,发现缺口时报错"Gap in migrations detected. Expected migration $expected, got $actual.",并要求迁移文件名中必须包含数字; - 输出:
generateMainDatabaseMigrations只负责把.sqm翻译成合法 SQL 产物,两者关注点不同,可以独立配置、独立使用。
在服务端多团队协作场景中,推荐的组合是:开发侧开启verifyMigrations/verifyDefinitions保证迁移链正确,同时配置migrationOutputDirectory将最终合法 SQL 提供给 DBA 或 Flyway 流水线消费。
使用注意与边界
- 输出目录每次生成前会被清空:这是 GenerateMigrationOutputTask.kt 的既定行为,请勿把其他文件放入该目录;
- 输出文件与迁移文件一一对应:文件名保持迁移文件主名,仅替换扩展名,因此依赖特定命名的工具(如 Flyway 的
V1__xxx.sql约定)需要在.sqm命名时就按版本号__描述的规范命名; migrationOutputFileFormat需以点号开头:默认".sql",如使用其他格式(如".txt")同样需要带上前导点;- 该功能对所有方言通用:包括 SQLite、MySQL、PostgreSQL(测试夹具
schema-output即使用 MySQL 方言验证),本文场景聚焦 docs/jvm_postgresql/migrations.md 所覆盖的 PostgreSQL 服务端用法; - 产物只含语句文本:输出不包含 SQLDelight 的类型映射与校验逻辑,外部服务执行时需自行保证语句在其目标数据库上的兼容性。
小结
围绕.sqm迁移文件因自定义 Kotlin 类型而丧失标准 SQL 身份这一核心痛点,SQLDelight 通过migrationOutputDirectory+migrationOutputFileFormat两个配置项与generateMainDatabaseMigrations任务,为 PostgreSQL 等服务端迁移场景提供了一条"编译期导出合法 SQL"的可靠路径。配合compileKotlin.dependsOn,Flyway 等外部服务无需再维护重复的 SQL 副本,即可在 Classpath 上直接消费经过验证的迁移产物。
如需继续深入,可阅读仓库中相关的配置与源码:Gradle 数据库配置总览、服务端 Schema 概念(Fresh/Migration Schema)、迁移中的自定义类型与乐观锁、GenerateMigrationOutputTask 实现 以及 迁移校验任务实现。
- 后端
- ORM
【免费下载链接】sqldelight
SQLDelight - Generates typesafe Kotlin APIs from SQL
相关推荐
如何用 Python + Selenium 配置大麦自动抢票:从安装到开抢的完整指南
如何用 Python + Selenium 配置大麦自动抢票:从安装到开抢的完整指南 开票按钮亮起的那一瞬间,手动操作往往跟不上网络延迟。ticket purc
后端ORMFlyway与PostgreSQL:无缝数据迁移指南
Flyway与PostgreSQL:无缝数据迁移指南 Flyway是Redgate推出的数据库迁移工具,能帮助开发者轻松管理PostgreSQL数据库架构变更。
数据库开发工具YimMenu Lua 脚本开发指南:memory 表——进程内存扫描、分配与动态 Hook/Call 全解析
YimMenu Lua 脚本开发指南:memory 表——进程内存扫描、分配与动态 Hook/Call 全解析 本文围绕 YimMenu 为 Lua 脚本开发者
后端ORM
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考