迁移 Migration
ACE ORM 提供两种 schema 管理模式:
| 模式 | 适用场景 |
|---|---|
| 自动同步(synchronize) | 开发/原型,启动时对比实体与 DB 结构,自动建表/补列 |
| 手动迁移(Migration) | 生产,版本化脚本精确控制 DDL,可上下滚动 |
快速开始:自动同步
在 ace.toml 开启 synchronize,框架每次启动自动对比并补全缺表/缺列:
toml
[datasource]
driver = "sqlite"
url = "app.db"
synchronize = true生产环境禁用 synchronize
synchronize = true 可能自动删列(配合 dropMissing)或改类型,生产环境务必改用手动迁移。
Migration 接口
每个迁移脚本实现 Migration 接口:
cangjie
package demo.migration
import ace_orm.*
public class CreateUsersTable <: Migration {
// 唯一版本号,建议用 YYYYMMDDHHMMSS 时间戳
public func version(): String { "20260701120000" }
// 描述(可选,用于打印)
public func name(): String { "CreateUsersTable" }
// 正向迁移:创建/修改 schema
public func up(qr: QueryRunner): Unit {
qr.createTable("users", [
ColumnSpec("id", DbType.TInt, true, ""),
ColumnSpec("email", DbType.TText, false, "", nullable: false, unique: true),
ColumnSpec("name", DbType.TText, false, ""),
ColumnSpec("createdAt", DbType.TInt, false, "")
])
}
// 反向迁移:撤销 up 的改动
public func down(qr: QueryRunner): Unit {
qr.exec("DROP TABLE IF EXISTS users")
}
}ColumnSpec 参数速查:
| 参数 | 类型 | 说明 |
|---|---|---|
name | String | 列名 |
dbType | DbType | 逻辑类型(方言自动映射为 DDL 类型) |
isPrimary | Bool | 是否主键/自增 |
defaultSql | String | DDL DEFAULT 值(原始 SQL) |
nullable | Bool | false → NOT NULL(默认 true) |
unique | Bool | 是否 UNIQUE(默认 false) |
DbType 枚举:
| 值 | DDL 对应 |
|---|---|
TInt | INTEGER / BIGINT / BIGINT |
TText | TEXT |
TReal | REAL / DOUBLE PRECISION / DOUBLE |
TBool | INTEGER / BOOLEAN / TINYINT(1) |
TBlob | BLOB / BYTEA / LONGBLOB |
TJson | TEXT / JSONB / JSON |
TDateTime | INTEGER / BIGINT / BIGINT(毫秒时间戳) |
MigrationRunner
MigrationRunner 负责执行/回滚迁移,内部维护 ace_migrations 历史表:
cangjie
package demo
import ace_orm.*
import demo.migration.*
func runMigrations(ds: DataSource): Unit {
let runner = MigrationRunner(ds)
// 注册所有迁移(按 version() 排序后顺序执行)
runner.add(CreateUsersTable())
runner.add(AddEmailIndexToUsers())
runner.add(CreatePostsTable())
// 执行所有未执行的迁移(幂等,已执行的跳过)
runner.runAll()
// 打印当前状态
for (status in runner.status()) {
println("${status.name} [${status.version}] - ${if (status.executed) { "done" } else { "pending" }}")
}
}runAll — 执行所有 pending 迁移
cangjie
runner.runAll()
// 1. 确保 ace_migrations 历史表存在(自动建,向后兼容旧表)
// 2. 按 version() 升序排列所有注册迁移
// 3. 查询历史表,跳过已执行的
// 4. 在同一事务内执行 up(),成功后写入历史记录revertLast — 回滚最后一个迁移
cangjie
runner.revertLast()
// 1. 查询 ace_migrations 最近一条(LIMIT 1)
// 2. 找到对应 Migration 实例
// 3. 执行 down(),成功后从历史表删除记录status — 查看执行状态
cangjie
let statuses = runner.status()
for (s in statuses) {
let flag = if (s.executed) { "[✓]" } else { "[ ]" }
println("${flag} ${s.version} ${s.name}")
}QueryRunner DDL 助手
Migration.up(qr) 和 Migration.down(qr) 中的 QueryRunner 提供以下 DDL/DML 助手:
裸 SQL
cangjie
// 执行 DDL/DML(返回受影响行数;非 DML 返回 -1)
qr.exec("CREATE INDEX idx_users_email ON users (email)")
// 带参数的 DML(参数化,防注入)
qr.exec("UPDATE users SET role = ? WHERE id = ?", [DbText("admin"), DbInt(1)])
// 查询
let rows = qr.query("SELECT id, email FROM users WHERE active = ?", [DbBool(true)])表操作
cangjie
// 建表(IF NOT EXISTS,幂等)
qr.createTable("roles", [
ColumnSpec("id", DbType.TInt, true, ""),
ColumnSpec("name", DbType.TText, false, "", nullable: false, unique: true)
])
// 表是否存在
if (!qr.hasTable("roles")) {
qr.createTable("roles", [...])
}列操作
cangjie
// 添加列(ALTER TABLE ... ADD COLUMN)
qr.addColumn("users", ColumnSpec("bio", DbType.TText, false, ""))
// 列是否存在
if (!qr.hasColumn("users", "bio")) {
qr.addColumn("users", ColumnSpec("bio", DbType.TText, false, ""))
}
// 列出现有列名
let cols = qr.columnNames("users")
// 列出列名 + 类型(供 schema diff 用)
let infos = qr.columnInfos("users")schema sync(程序化同步)
cangjie
// 对比「目标」与「现状」,只补不删(dropMissing 默认 false)
qr.syncTable("users", [
ColumnSpec("id", DbType.TInt, true, ""),
ColumnSpec("email", DbType.TText, false, "", nullable: false, unique: true),
ColumnSpec("bio", DbType.TText, false, "") // 新列会被自动 ADD COLUMN
])
// 危险模式:同时删除实体中不存在的列
qr.syncTable("users", targetCols, dropMissing: true)迁移文件组织建议
src/
├── migration/
│ ├── v20260701120000_create_users.cj
│ ├── v20260702080000_add_email_index.cj
│ ├── v20260703090000_create_posts.cj
│ └── index.cj ← 统一注册所有迁移
└── main.cjmigration/index.cj:
cangjie
package demo.migration
import ace_orm.*
public func registerAll(runner: MigrationRunner): Unit {
runner.add(V20260701120000CreateUsers())
runner.add(V20260702080000AddEmailIndex())
runner.add(V20260703090000CreatePosts())
}main.cj 中启动:
cangjie
import ace_orm.*
import demo.migration.*
main() {
let ds = DataSource("sqlite", "app.db")
let runner = MigrationRunner(ds)
registerAll(runner)
runner.runAll()
// ...启动 HTTP 服务
}历史表结构
ACE ORM 自动维护 ace_migrations 表:
sql
CREATE TABLE IF NOT EXISTS ace_migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
executedAt INTEGER NOT NULL
);向后兼容
若历史表由旧版本(无 executedAt 列)创建,MigrationRunner 启动时自动检测并 ALTER TABLE ADD COLUMN executedAt。
完整迁移示例
cangjie
package demo.migration
import ace_orm.*
// 建表
public class V20260701120000CreateUsers <: Migration {
public func version(): String { "20260701120000" }
public func name(): String { "CreateUsers" }
public func up(qr: QueryRunner): Unit {
qr.createTable("users", [
ColumnSpec("id", DbType.TInt, true, ""),
ColumnSpec("email", DbType.TText, false, "", nullable: false, unique: true),
ColumnSpec("name", DbType.TText, false, ""),
ColumnSpec("role", DbType.TText, false, "user"),
ColumnSpec("createdAt", DbType.TInt, false, "")
])
qr.exec("CREATE INDEX idx_users_email ON users (email)")
}
public func down(qr: QueryRunner): Unit {
qr.exec("DROP TABLE IF EXISTS users")
}
}
// 添加列
public class V20260702080000AddBio <: Migration {
public func version(): String { "20260702080000" }
public func name(): String { "AddBioToUsers" }
public func up(qr: QueryRunner): Unit {
qr.addColumn("users", ColumnSpec("bio", DbType.TText, false, ""))
}
public func down(qr: QueryRunner): Unit {
qr.exec("ALTER TABLE users DROP COLUMN bio")
}
}
// 数据迁移
public class V20260703090000SeedRoles <: Migration {
public func version(): String { "20260703090000" }
public func name(): String { "SeedRoles" }
public func up(qr: QueryRunner): Unit {
qr.exec("INSERT INTO roles (name) VALUES (?)", [DbText("admin")])
qr.exec("INSERT INTO roles (name) VALUES (?)", [DbText("user")])
}
public func down(qr: QueryRunner): Unit {
qr.exec("DELETE FROM roles WHERE name IN ('admin', 'user')")
}
}