Skip to content

迁移 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 参数速查:

参数类型说明
nameString列名
dbTypeDbType逻辑类型(方言自动映射为 DDL 类型)
isPrimaryBool是否主键/自增
defaultSqlStringDDL DEFAULT 值(原始 SQL)
nullableBoolfalse → NOT NULL(默认 true
uniqueBool是否 UNIQUE(默认 false

DbType 枚举:

DDL 对应
TIntINTEGER / BIGINT / BIGINT
TTextTEXT
TRealREAL / DOUBLE PRECISION / DOUBLE
TBoolINTEGER / BOOLEAN / TINYINT(1)
TBlobBLOB / BYTEA / LONGBLOB
TJsonTEXT / JSONB / JSON
TDateTimeINTEGER / 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.cj

migration/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')")
    }
}

基于 Apache-2.0 许可证发布