简介

Schema 迁移

本指南涵盖编写和运行迁移。你将学会如何:

  • 创建迁移并理解其结构
  • 使用 BaseSchema API 来演进数据库 schema
  • 使用 this.defer 安全地执行数据迁移
  • 运行、回滚和刷新迁移
  • 使用应用锁、dry run 和回滚限制来处理生产安全性
  • 跨多个数据库连接管理迁移
  • 从应用代码以编程方式执行迁移

概述

Schema 迁移是版本控制的脚本,随时间演进你的数据库 schema。每个迁移是一个 TypeScript 文件,包含 up()down() 方法,用于应用或回滚 schema 变更。AdonisJS 在 adonis_schema 表中跟踪哪些迁移已运行,因此每个迁移在每个数据库上精确运行一次。

Lucid 是迁移优先的:你编写迁移来演进数据库,运行它们,然后 Lucid 从结果表生成带类型的 schema 类。模型继承这些生成的类。参见简介了解更广泛的理念,以及schema 生成指南了解迁移后重新生成的工作流。

创建迁移

使用 make:migration 命令在 database/migrations 中生成一个新的迁移文件。文件名以时间戳为前缀,因此文件按创建顺序运行。

terminal
node ace make:migration users --create=users
// CREATE: database/migrations/1720000000000_create_users_table.ts

在修改现有表时,传递 --alter=table_name 而不是 --create。参见命令参考了解完整标志列表。

你也可以使用 node ace make:model User --migration 在模型旁边生成迁移。

迁移类

迁移类继承 BaseSchema 并且必须实现 up(应用变更)和 down(回滚变更)。该类还暴露了用于原始 SQL、延迟操作和直接查询客户端访问的助手。

database/migrations/1720000000000_create_users_table.ts
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'users'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('email').unique().notNullable()
table.string('password').notNullable()
table.timestamp('created_at', { useTz: true })
table.timestamp('updated_at', { useTz: true })
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}

tableName 属性是一个约定而非特性。在类顶部设置一次,以便相同的值可以在 updown 和任何辅助方法中重用。

updown 内部,类上可以使用以下成员:

this.schema

Knex schema 构建器,用于定义 createTablealterTablerenameTabledropTable、索引和其他 DDL 操作。参见 schema 构建器参考table 构建器参考了解完整方法集。

this.now(precision?)

返回一个原始 CURRENT_TIMESTAMP 表达式,用于作为列默认值。传入可选的精度(小数秒位数)。

table.timestamp('created_at').defaultTo(this.now())

this.raw(sql, bindings?)

返回一个原始 SQL 片段,用于在 schema 操作或列默认值中使用。接受与 raw 查询构建器相同的绑定格式。

table.uuid('id').defaultTo(this.raw('gen_random_uuid()'))

this.knex()

返回迁移连接的底层 Knex 查询构建器。用于 schema 构建器未暴露的高级操作。

this.db

迁移连接的 QueryClientContract。在 this.defer 回调内部使用它来读取和写入数据。参见数据迁移

this.dryRun

当迁移在 dry-run 模式下执行时为 true。在应在 dry run 期间跳过的自定义逻辑内部读取此标志。

static disableTransactions

在类上设置为 true 以跳过将此迁移包装在事务中。当迁移包含无法在事务中运行的语句时使用(例如 PostgreSQL 的 CREATE INDEX CONCURRENTLY)。

export default class extends BaseSchema {
static disableTransactions = true
// ...
}

常见 schema 操作

Schema 构建器暴露了你最常用的操作。下面的示例展示了基本形式;有关完整 API,请参见 schema 构建器table 构建器参考。

// 创建表
this.schema.createTable('posts', (table) => {
table.increments('id')
table.string('title').notNullable()
table.timestamps(true, true)
})
// 修改现有表
this.schema.alterTable('users', (table) => {
table.string('first_name')
table.string('last_name')
table.dropColumn('name')
})
// 重命名表
this.schema.renameTable('user', 'users')
// 删除表
this.schema.dropTable('users')

数据迁移

Schema 迁移有时需要移动或转换数据,而不仅仅是更改结构。常见情况包括从现有列回填新列、将列拆分为两个,或在删除源表之前将数据复制到另一个表。

数据操作属于 this.defer(callback) 内部。延迟回调接收查询客户端,并且仅在迁移实际执行时运行,因此在 --dry-run 期间它们被跳过。它们还按照注册顺序运行,与 schema 操作交错进行。

database/migrations/1720000010000_split_user_name.ts
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
async up() {
this.schema.alterTable('users', (table) => {
table.string('first_name')
table.string('last_name')
})
this.defer(async (db) => {
const users = await db.from('users').select('id', 'name')
for (const user of users) {
const [first, ...rest] = user.name.split(' ')
await db
.from('users')
.where('id', user.id)
.update({ first_name: first, last_name: rest.join(' ') })
}
})
this.schema.alterTable('users', (table) => {
table.dropColumn('name')
})
}
async down() {
this.schema.alterTable('users', (table) => {
table.string('name')
})
this.defer(async (db) => {
const users = await db.from('users').select('id', 'first_name', 'last_name')
for (const user of users) {
await db
.from('users')
.where('id', user.id)
.update({ name: `${user.first_name} ${user.last_name}`.trim() })
}
})
this.schema.alterTable('users', (table) => {
table.dropColumn('first_name')
table.dropColumn('last_name')
})
}
}

回调内部的 db 参数与作为 this.db 可用的 QueryClientContract 是同一个。你可以在 defer 内部使用数据库查询构建器insert 查询构建器原始查询

对于生产表上的大型数据迁移,分批处理你的工作以保持事务简短并避免长时间的表锁。考虑将数据迁移与 schema 变更放在单独的迁移文件中,以便每个步骤可以独立审查和部署。

运行和回滚迁移

Lucid 为迁移生命周期的每个阶段提供了 Ace 命令。每个命令的标志在命令参考中有文档说明;本节解释何时使用哪个命令。

migration:run 按顺序应用每个待处理的迁移,将每个迁移记录在 adonis_schema 表中,并重新生成 database/schema.ts,以便你的模型继承新的列类型。

node ace migration:run

migration:rollback 通过调用每个迁移的 down 方法来回滚最近一批迁移。传递 --step=N 来回滚特定数量的文件,或 --batch=N 来回滚到特定批次号。

node ace migration:rollback
node ace migration:rollback --step=1
node ace migration:rollback --batch=0 # 回滚所有内容

migration:resetmigration:rollback --batch=0 的简写。

migration:refresh 回滚所有迁移并重新运行它们,在每个文件上调用 down 然后 up。在开发中,当你想要在保留迁移历史的同时进行干净的重建时很有用。传递 --seed 以在刷新后也运行种子。

migration:fresh 删除数据库中的所有表并从头重新运行迁移。它不调用 down,因此比 refresh 更快,即使某些 down 方法有问题也能工作。传递 --seed 也运行种子。

对于具有长迁移历史记录的项目,在 CI 或新贡献者的机器上重放每个文件会成为瓶颈。参见 schema 导出指南了解从 SQL 快照引导新数据库以及将旧迁移压缩为单个基线。

migration:status 列出每个迁移文件及其当前状态。

node ace migration:status

输出按批次分组迁移,并将每个标记为 completedpendingerror

跟踪如何工作

Lucid 将每个已应用的迁移记录在 adonis_schema 表中:

描述
name相对于项目根目录的迁移文件路径
batch批次号,每次调用 migration:run 递增一
migration_time迁移应用时的时间戳

migration:rollback 使用批次号来选择要回滚的文件。一次 migration:run 调用产生一个批次,无论运行了多少个文件。

生产安全性

在生产数据库上的迁移比本地开发的风险更高。Lucid 提供了几种机制来使它们更安全。

应用锁

在运行迁移之前,Lucid 在数据库上获取一个应用锁,以防止并发迁移运行相互竞争(例如,两个 CI 部署同时推出)。应用锁在 PostgreSQL 和 MySQL 上受支持。

acquired migration lock

如果你需要跳过锁(例如,针对不支持应用锁的数据库运行迁移,或者先前的崩溃运行留下了过时的锁),传递 --disable-locks

node ace migration:run --disable-locks

在生产中禁用回滚

在连接的 migrations 配置中设置 disableRollbacksInProduction: true,以在 NODE_ENV=production 时拒绝回滚命令。这可以防止对生产数据库的意外破坏性操作。参见配置指南了解完整的迁移配置参考。

config/database.ts
{
migrations: {
disableRollbacksInProduction: true,
}
}

其推论是生产 schema 变更应始终向前推进。当你需要撤销先前的迁移时,编写一个新迁移来显式反向它,而不是回滚。这使迁移历史在每个环境中保持一致。

Dry run

传递 --dry-runmigration:runmigration:rollback 以打印 SQL 而不执行它。在代码审查中或应用高风险迁移之前使用它,以验证生成的 SQL 符合你的意图。

node ace migration:run --dry-run

this.defer 内部定义的操作在 dry run 期间被跳过,因为它们执行自己的查询,dry-run 管道无法检查这些查询。在审查包含数据回填的迁移时,请相应规划。

逐迁移事务

默认情况下,Lucid 将每个迁移文件包装在事务中,以便部分失败可以干净地回滚。某些操作无法在事务中运行——PostgreSQL 的 CREATE INDEX CONCURRENTLY 是最常见的例子。通过设置 static disableTransactions = true 在类上为这些迁移禁用事务。

export default class extends BaseSchema {
static disableTransactions = true
async up() {
this.schema.raw('CREATE INDEX CONCURRENTLY posts_title_idx ON posts (title)')
}
}

要全局为所有迁移禁用事务,在连接的 migrations 配置中设置 disableTransactions: true

多连接

使用多个数据库连接的应用可以为每个连接保留单独的迁移,或跨连接共享迁移。

每个连接单独的迁移

每个连接指向自己的迁移目录。当每个数据库有不同的表时使用。

config/database.ts
{
users: {
client: 'pg',
migrations: {
paths: ['./database/users/migrations'],
},
},
products: {
client: 'pg',
migrations: {
paths: ['./database/products/migrations'],
},
},
}

传递 --connection=name 来针对特定连接生成和运行迁移。

node ace make:migration --connection=products
node ace migration:run --connection=products

跨连接共享迁移

当相同的 schema 跨连接复制时(例如,每个租户一个数据库的多租户设置),共享单个迁移目录并在运行时切换连接。

node ace migration:run --connection=tenant_a
node ace migration:run --connection=tenant_b

迁移文件针对所选连接的数据库运行,使用自己的 adonis_schema 表来跟踪状态。

以编程方式运行迁移

对于需要从 CLI 之外运行迁移的工具(管理面板、设置向导、自定义部署脚本),直接使用 MigrationRunner 类。

app/controllers/migrations_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import db from '@adonisjs/lucid/services/db'
import app from '@adonisjs/core/services/app'
import { MigrationRunner } from '@adonisjs/lucid/migration'
export default class MigrationsController {
async run({ response }: HttpContext) {
const migrator = new MigrationRunner(db, app, {
direction: 'up',
dryRun: false,
})
await migrator.run()
return response.ok({
status: migrator.status,
files: migrator.migratedFiles,
error: migrator.error?.message,
})
}
}

运行器接受以下选项:

direction

'up' 应用待处理迁移,'down' 回滚。必需。

dryRun

当为 true 时,收集 SQL 而不执行。收集到的查询出现在 migratedFiles[name].queries 中。

connectionName

针对非默认连接运行。

disableLocks

跳过在迁移运行周围获取的应用锁。

run() 解析后,检查运行器的属性以查看发生了什么。

migrator.status

'pending''completed''error''skipped' 之一。pending 表示 run() 尚未被调用,completed 表示迁移成功应用,error 表示迁移抛出异常,skipped 表示没有需要做的事。

migrator.migratedFiles

以迁移文件名为键的对象,每个值包含 status'pending''completed''error')、batchfile 元数据和 queries(仅在 dry-run 模式下填充)。

migrator.error

status === 'error' 时,失败的迁移抛出的错误。

migrator.getList()

返回与 migration:status 显示的相同的迁移和状态列表。用于构建状态 UI。

运行器是一个 EventEmitter,为每个文件发出 migration:startmigration:completed 事件,这让你可以将进度流式传输到 UI。