迁移
MikroORM 通过 umzug 集成了对迁移的支持。它允许你根据当前模式差异生成迁移。
¥MikroORM has integrated support for migrations via umzug. It allows you to generate migrations based on the current schema difference.
要使用迁移,你需要先为 SQL 驱动程序安装 @mikro-orm/migrations
包或为 MongoDB 安装 @mikro-orm/migrations-mongodb
,并在 ORM 配置中注册 Migrator
扩展。
¥To use migrations, you need to first install @mikro-orm/migrations
package for SQL driver or @mikro-orm/migrations-mongodb
for MongoDB, and register the Migrator
extension in your ORM config.
import { Migrator } from '@mikro-orm/migrations'; // or `@mikro-orm/migrations-mongodb`
export default defineConfig({
// ...
extensions: [Migrator],
})
自 v5 起,不再可能使用全局身份映射。
¥Since v5, migrations are stored without an extension.
默认情况下,每个迁移都将在一个事务中执行,并且所有迁移都将封装在一个主事务中,因此如果其中一个失败,则所有内容都将回滚。
¥By default, each migration will be executed inside a transaction, and all of them will be wrapped in one master transaction, so if one of them fails, everything will be rolled back.
迁移类
¥Migration class
迁移是扩展迁移抽象类的类:
¥Migrations are classes that extend Migration abstract class:
import { Migration } from '@mikro-orm/migrations';
export class Migration20191019195930 extends Migration {
async up(): Promise<void> {
this.addSql('select 1 + 1');
}
}
为了支持撤消更改,你可以实现 down
方法,该方法默认会引发错误。
¥To support undoing those changed, you can implement the down
method, which throws an error by default.
迁移默认封装在事务中。你可以通过实现 isTransactional(): boolean
方法来在每次迁移的基础上覆盖此行为。
¥Migrations are by default wrapped in a transaction. You can override this behaviour on per migration basis by implementing the isTransactional(): boolean
method.
Configuration
对象和驱动程序实例在 Migration
类上下文中可用。
¥Configuration
object and driver instance are available in the Migration
class context.
你可以通过 Migration.execute()
方法在迁移中执行查询,该方法将在与迁移的其余部分相同的事务中运行查询。Migration.addSql()
方法还接受 knex 的实例。可以通过 Migration.getKnex()
访问 Knex 实例;
¥You can execute queries in the migration via Migration.execute()
method, which will run queries in the same transaction as the rest of the migration. The Migration.addSql()
method also accepts instances of knex. Knex instance can be accessed via Migration.getKnex()
;
使用 EntityManager
¥Working with EntityManager
虽然迁移的目的主要是更改你的 SQL 模式,但你也可以使用它们来修改数据,无论是使用 this.execute()
还是通过 EntityManager
:
¥While the purpose of migrations is mainly to alter your SQL schema, you can as well use them to modify your data, either by using this.execute()
, or through an EntityManager
:
可以在迁移中使用 EntityManager
,但不建议这样做,因为当你的元数据随时间发生变化时,它可能会导致错误,因为这取决于你当前签出的应用状态,而不是生成迁移的时间。你应该更喜欢在迁移中使用原始查询。
¥Using the EntityManager
in migrations is possible, but discouraged, as it can lead to errors when your metadata change over time, since this will depend on your currently checked out app state, not on the time when the migration was generated. You should prefer using raw queries in your migrations.
import { Migration } from '@mikro-orm/migrations';
import { User } from '../entities/User';
export class Migration20191019195930 extends Migration {
async up(): Promise<void> {
const em = this.getEntityManager();
em.create(User, { ... });
await em.flush();
}
}
初始迁移
¥Initial migration
这是可选的,仅在特定用例中需要,即实体和模式都已存在时。
¥This is optional and only needed for the specific use case, when both entities and schema already exist.
如果你想开始使用迁移,并且已经生成了模式,则可以通过创建所谓的初始迁移来实现:
¥If you want to start using migrations, and you already have the schema generated, you can do so by creating so-called initial migration:
仅当之前没有生成或执行迁移时,才能创建初始迁移。
¥Initial migration can be created only if there are no migrations previously generated or executed.
npx mikro-orm migration:create --initial
这将创建初始迁移,其中包含来自 schema:create
命令的架构转储。迁移将自动标记为已执行。
¥This will create the initial migration, containing the schema dump from schema:create
command. The migration will be automatically marked as executed.
快照
¥Snapshots
创建新迁移将自动将目标模式快照保存到迁移文件夹中。如果你尝试创建新的迁移,而不是使用当前数据库模式,则将使用此快照。这意味着如果你在运行待处理的迁移之前尝试创建新的迁移,你仍然可以获得正确的架构差异。
¥Creating new migration will automatically save the target schema snapshot into migrations folder. This snapshot will be then used if you try to create new migration, instead of using current database schema. This means that if you try to create new migration before you run the pending ones, you still get the right schema diff.
快照应该像常规迁移文件一样进行版本控制。
¥Snapshots should be versioned just like the regular migration files.
可以通过 ORM 配置中的 migrations.snapshot: false
禁用快照。
¥Snapshotting can be disabled via migrations.snapshot: false
in the ORM config.
配置
¥Configuration
自 v5 以来,MikroORM 还支持定义可以存在于多个模式中的实体。
¥Since v5,
umzug
3.0 is used, andpattern
option has been replaced withglob
.
migrations.path
和migrations.pathTs
在实体发现中的工作方式与entities
和entitiesTs
相同。¥
migrations.path
andmigrations.pathTs
works the same way asentities
andentitiesTs
in entity discovery.
await MikroORM.init({
// default values:
migrations: {
tableName: 'mikro_orm_migrations', // name of database table with log of executed transactions
path: './migrations', // path to the folder with migrations
pathTs: undefined, // path to the folder with TS migrations (if used, you should put path to compiled files in `path`)
glob: '!(*.d).{js,ts}', // how to match migration files (all .js and .ts files, but not .d.ts)
transactional: true, // wrap each migration in a transaction
disableForeignKeys: true, // wrap statements with `set foreign_key_checks = 0` or equivalent
allOrNothing: true, // wrap all migrations in master transaction
dropTables: true, // allow to disable table dropping
safe: false, // allow to disable table and column dropping
snapshot: true, // save snapshot when creating new migrations
emit: 'ts', // migration generation mode
generator: TSMigrationGenerator, // migration generator, e.g. to allow custom formatting
},
})
在生产中运行迁移
¥Running migrations in production
在生产环境中,你可能想要使用编译的迁移文件。自 v5 起,新实体在运行时(执行插入查询之前)根据实体元数据进行验证。
¥In production environment you might want to use compiled migration files. Since v5, this should work almost out of box, all you need to do is to configure the migration path accordingly:
import { MikroORM, Utils } from '@mikro-orm/core';
await MikroORM.init({
migrations: {
path: 'dist/migrations',
pathTs: 'src/migrations',
},
// or alternatively
// migrations: {
// path: Utils.detectTsNode() ? 'src/migrations' : 'dist/migrations',
// },
// ...
});
这应该允许使用 CLI 生成 TS 迁移文件(因为在 CLI 中你可能已启用 TS 支持),同时在生产中使用编译的 JS 文件,其中 ts-node 未注册。
¥This should allow using CLI to generate TS migration files (as in CLI you probably have TS support enabled), while using compiled JS files in production, where ts-node is not registered.
使用自定义 MigrationGenerator
¥Using custom MigrationGenerator
当你生成新的迁移时,MigrationGenerator
类负责生成文件内容。你可以提供自己的实现来执行诸如格式化 SQL 语句之类的操作。
¥When you generate new migrations, MigrationGenerator
class is responsible for generating the file contents. You can provide your own implementation to do things like formatting the SQL statement.
import { TSMigrationGenerator } from '@mikro-orm/migrations';
import { format } from 'sql-formatter';
class CustomMigrationGenerator extends TSMigrationGenerator {
generateMigrationFile(className: string, diff: { up: string[]; down: string[] }): string {
const comment = '// this file was generated via custom migration generator\n\n';
return comment + super.generateMigrationFile(className, diff);
}
createStatement(sql: string, padLeft: number): string {
sql = format(sql, { language: 'postgresql' });
// a bit of indenting magic
sql = sql.split('\n').map((l, i) => i === 0 ? l : `${' '.repeat(padLeft + 13)}${l}`).join('\n');
return super.createStatement(sql, padLeft);
}
}
await MikroORM.init({
// ...
migrations: {
generator: CustomMigrationGenerator,
},
});
通过 CLI 使用
¥Using via CLI
你可以通过 CLI 使用它:
¥You can use it via CLI:
npx mikro-orm migration:create # Create new migration with current schema diff
npx mikro-orm migration:up # Migrate up to the latest version
npx mikro-orm migration:down # Migrate one step down
npx mikro-orm migration:list # List all executed migrations
npx mikro-orm migration:check # Check if schema is up to date
npx mikro-orm migration:pending # List all pending migrations
npx mikro-orm migration:fresh # Drop the database and migrate up to the latest version
要创建空白迁移文件,你可以使用
npx mikro-orm migration:create --blank
。¥To create blank migration file, you can use
npx mikro-orm migration:create --blank
.
对于 migration:up
和 migration:down
命令,你可以指定 --from
(-f
)、--to
(-t
) 和 --only
(-o
) 选项以仅运行迁移的子集:
¥For migration:up
and migration:down
commands you can specify --from
(-f
), --to
(-t
) and --only
(-o
) options to run only a subset of migrations:
npx mikro-orm migration:up --from 2019101911 --to 2019102117 # the same as above
npx mikro-orm migration:up --only 2019101923 # apply a single migration
npx mikro-orm migration:down --to 0 # migrate down all migrations
要运行 TS 迁移文件,请确保你的项目中安装了
ts-node
,CLI 将自 v6.3 起自动注册它。¥To run TS migration files, make sure you have
ts-node
installed in your project, the CLI will register it automatically since v6.3.
对于 migration:fresh
命令,你可以指定 --seed
在迁移后为数据库播种。
¥For the migration:fresh
command you can specify --seed
to seed the database after migrating.
npx mikro-orm migration:fresh --seed # seed the database with the default database seeder
npx mikro-orm migration:fresh --seed=UsersSeeder # seed the database with the UsersSeeder
你可以在 orm 配置中使用键
config.seeder.defaultSeeder
指定默认数据库种子¥You can specify the default database seeder in the orm config with the key
config.seeder.defaultSeeder
以编程方式使用迁移器
¥Using the Migrator programmatically
或者你可以创建一个简单的脚本,像这样初始化 MikroORM:
¥Or you can create a simple script where you initialize MikroORM like this:
import { MikroORM } from '@mikro-orm/core';
import { Migrator } from '@mikro-orm/migrations';
(async () => {
const orm = await MikroORM.init({
extensions: [Migrator],
dbName: 'your-db-name',
// ...
});
const migrator = orm.getMigrator();
await migrator.createMigration(); // creates file Migration20191019195930.ts
await migrator.up(); // runs migrations up to the latest
await migrator.up('name'); // runs only given migration, up
await migrator.up({ to: 'up-to-name' }); // runs migrations up to given version
await migrator.down(); // migrates one step down
await migrator.down('name'); // runs only given migration, down
await migrator.down({ to: 'down-to-name' }); // runs migrations down to given version
await migrator.down({ to: 0 }); // migrates down to the first version
await orm.close(true);
})();
然后通过 ts-node
运行此脚本(或将其编译为纯 JS 并使用 node
):
¥Then run this script via ts-node
(or compile it to plain JS and use node
):
$ ts-node migrate
提供事务上下文
¥Providing transaction context
在某些情况下,你可能希望自己控制事务上下文:
¥In some cases, you might want to control the transaction context yourself:
await orm.em.transactional(async em => {
await migrator.up({ transaction: em.getTransactionContext() });
});
静态导入迁移
¥Importing migrations statically
如果你不想动态导入文件夹(例如,在使用 webpack 打包代码时),你可以直接导入迁移。你可以使用显式迁移名称或隐式文件名作为迁移名称来执行此操作。
¥If you do not want to dynamically import a folder (e.g. when bundling your code with webpack) you can import migrations directly. You can do that with an explicit migration name or the implicit filename as migration name.
import { MikroORM } from '@mikro-orm/core';
import { Migrator } from '@mikro-orm/migrations';
import { Migration20191019195930 } from '../migrations/Migration20191019195930.ts';
import { Migration20191019195931 } from '../migrations/Migration20191019195931.ts';
await MikroORM.init({
extensions: [Migrator],
migrations: {
migrationsList: [
// explicit migration name
{
name: 'CustomMigrationName',
class: Migration20191019195930,
},
// implicit migration name
Migration20191019195931
],
},
});
借助 webpack 的上下文模块 api,你可以动态导入迁移,从而可以导入文件夹中的所有文件。
¥With the help of webpack's context module api you can dynamically import the migrations making it possible to import all files in a folder.
import { MikroORM } from '@mikro-orm/core';
import { Migrator } from '@mikro-orm/migrations';
import { basename } from 'path';
const migrations = {};
function importAll(r) {
r.keys().forEach(
(key) => (migrations[basename(key)] = Object.values(r(key))[0])
);
}
importAll(require.context('../migrations', false, /\.ts$/));
const migrationsList = Object.keys(migrations).map((migrationName) => ({
name: migrationName,
class: migrations[migrationName],
}));
await MikroORM.init({
extensions: [Migrator],
migrations: {
migrationsList,
},
});
使用自定义迁移名称
¥Using custom migration names
自 v5.7 起,你可以通过 --name
CLI 选项指定自定义迁移名称。它将附加到生成的前缀:
¥Since v5.7, you can specify a custom migration name via --name
CLI option. It will be appended to the generated prefix:
# generates file Migration20230421212713_add_email_property_to_user_table.ts
npx mikro-orm migration:create --name=add_email_property_to_user_table
你可以通过利用 fileName
回调自定义迁移文件的命名约定,甚至可以使用它来强制使用名称进行迁移:
¥You can customize the naming convention for your migration file by utilizing the fileName
callback, or even use it to enforce migrations with names:
migrations: {
fileName: (timestamp: string, name?: string) => {
// force user to provide the name, otherwise you would end up with `Migration20230421212713_undefined`
if (!name) {
throw new Error('Specify migration name via `mikro-orm migration:create --name=...`');
}
return `Migration${timestamp}_${name}`;
},
},
覆盖 migrations.fileName
策略时,请记住你的迁移文件需要可排序,你永远不应使用自定义 name
选项开始文件名,因为这可能会导致错误的执行顺序。
¥When overriding the migrations.fileName
strategy, keep in mind that your migration files need to be sortable, you should never start the filename with the custom name
option as it could result in wrong order of execution.
MongoDB 支持
¥MongoDB support
v5.3 中添加了对 MongoDB 中迁移的支持。它使用自己的包:@mikro-orm/migrations-mongodb
,并且应该与当前的 CLI 命令兼容。使用 this.driver
或 this.getCollection()
来操作数据库。
¥Support for migrations in MongoDB has been added in v5.3. It uses its own package: @mikro-orm/migrations-mongodb
, and should be otherwise compatible with the current CLI commands. Use this.driver
or this.getCollection()
to manipulate with the database.
事务
¥Transactions
Migrator
的默认选项将使用事务,这些选项在 mongo 中施加了一些额外的要求,即集合需要预先存在,并且你需要运行副本集。你可能想要禁用 migrations: { transactional: false }
的事务。
¥The default options for Migrator
will use transactions, and those impose some additional requirements in mongo, namely the collections need to exist upfront, and you need to run a replicaset. You might want to disable transactions for migrations: { transactional: false }
.
await this.driver.nativeDelete('Book', { foo: true }, { ctx: this.ctx });
你需要手动为查询提供事务上下文,可以通过驱动程序方法的 ctx
选项,也可以在使用 this.getCollection()
方法时通过 MongoDB session
选项。
¥You need to provide the transaction context manually to your queries, either via the ctx
option of the driver methods, or via the MongoDB session
option when using the this.getCollection()
method.
await this.getCollection('Book').updateMany({}, { $set: { updatedAt: new Date() } }, { session: this.ctx });
迁移类
¥Migration class
mongo 中的迁移示例:
¥Example migration in mongo:
import { Migration } from '@mikro-orm/migrations-mongodb';
export class MigrationTest1 extends Migration {
async up(): Promise<void> {
// use `this.getCollection()` to work with the mongodb collection directly
await this.getCollection('Book').updateMany({}, { $set: { updatedAt: new Date() } }, { session: this.ctx });
// or use `this.driver` to work with the `MongoDriver` API instead
await this.driver.nativeDelete('Book', { foo: true }, { ctx: this.ctx });
}
}
局限性
¥Limitations
MySQL
在 MySQL 中无法回滚 DDL 更改。这些查询会自动强制隐式提交,因此事务无法按预期工作。
¥There is no way to rollback DDL changes in MySQL. An implicit commit is forced for those queries automatically, so transactions are not working as expected.
MongoDB
-
不支持嵌套事务
¥no nested transaction support
-
没有架构差异
¥no schema diffing
-
仅生成空白迁移
¥only blank migrations are generated
调试
¥Debugging
有时模式差异可能无法按预期工作并会产生不必要的查询。这通常是你设置属性的 columnType
或 default/defaultRaw
选项的方式存在的问题。你可以使用 MIKRO_ORM_CLI_VERBOSE
环境变量来启用 CLI 的详细日志记录。这反过来又可以记录用于提取当前模式的底层查询和 SchemaComparator
中的日志,这应该有助于你理解为什么 ORM 将两列视为不同以及哪些特定选项不同。
¥Sometimes the schema diffing might not work as expected and will produce unwanted queries. Often this is a problem with how you set up the columnType
or default/defaultRaw
options of your properties. You can use the MIKRO_ORM_CLI_VERBOSE
environment variable to enable verbose logging of the CLI. This, in turn, enables logging of both the underlying queries used to extract the current schema and the logs in the SchemaComparator
, which should help you understand why the ORM sees two columns as different and what particular options are different.
使用
schema:update
时,迁移问题的调试更容易,因为你可以跳过其上的 Migrator 层并测试发生这些问题的实际层。¥Debugging issues with migrations is easier when you use
schema:update
, as you skip the Migrator layer on top of it and test the actual layer where those problems occur.
$ MIKRO_ORM_CLI_VERBOSE=1 npx mikro-orm schema:update --dump