播种
初始化应用或测试时,为数据库创建示例数据可能非常耗时。解决方案是使用种子。为你的实体创建工厂并在种子脚本中使用它们或组合多个种子脚本。
¥When initializing your application or testing it can be exhausting to create sample data for your database. The solution is to use seeding. Create factories for your entities and use them in the seed script or combine multiple seed scripts.
要使用种子,你需要先安装 @mikro-orm/seeder,并在 ORM 配置中注册 SeedManager 扩展。
¥To use seeder, you need to first install @mikro-orm/seeder, and register the SeedManager extension in your ORM config.
import { SeedManager } from '@mikro-orm/seeder';
export default defineConfig({
// ...
extensions: [SeedManager],
})
配置
¥Configuration
seeder.path和seeder.pathTs在实体发现中的工作方式与entities和entitiesTs相同。¥
seeder.pathandseeder.pathTsworks the same way asentitiesandentitiesTsin entity discovery.
种子有一些默认设置,可以通过 MikroORM 配置轻松更改。下方列出了配置选项及其默认值。
¥The seeder has a few default settings that can be changed easily through the MikroORM config. Underneath, you find the configuration options with their defaults.
MikroORM.init({
seeder: {
path: './seeders', // path to the folder with seeders
pathTs: undefined, // path to the folder with TS seeders (if used, you should put path to compiled files in `path`)
defaultSeeder: 'DatabaseSeeder', // default seeder class name
glob: '!(*.d).{js,ts}', // how to match seeder files (all .js and .ts files, but not .d.ts)
emit: 'ts', // seeder generation mode
fileName: (className: string) => className, // seeder file naming convention
},
});
你也可以使用 环境变量 覆盖这些选项:
¥You can also override those options using the environment variables:
-
MIKRO_ORM_SEEDER_PATH -
MIKRO_ORM_SEEDER_PATH_TS -
MIKRO_ORM_SEEDER_EMIT -
MIKRO_ORM_SEEDER_GLOB -
MIKRO_ORM_SEEDER_DEFAULT_SEEDER
种子
¥Seeders
种子类包含一个方法 run。当你使用命令 npx mikro-orm seeder:run 时,将调用此方法。在 run 方法中,你可以定义如何以及要将哪些数据插入数据库。你可以使用 EntityManager 创建实体,也可以使用 工厂。
¥A seeder class contains one method run. This method is called when you use the command npx mikro-orm seeder:run. In the run method you define how and what data you want to insert into the database. You can create entities using the EntityManager or you can use Factories.
你可以使用以下 CLI 命令创建自己的种子类:
¥You can create your own seeder classes using the following CLI command:
npx mikro-orm seeder:create DatabaseSeeder # generates the class DatabaseSeeder
npx mikro-orm seeder:create test # generates the class TestSeeder
npx mikro-orm seeder:create project-names # generates the class ProjectNamesSeeder
这将创建一个新的种子类。默认情况下,它将在 ./seeders/ 目录中生成。你可以使用密钥 seeder.path 或使用 环境变量 MIKRO_ORM_SEEDER_PATH 在配置中配置目录。你可以使用名称、类名或带连字符的名称调用 seeder:create 命令。
¥This creates a new seeder class. By default, it will be generated in the ./seeders/ directory. You can configure the directory in the config with the key seeder.path or using the environment variable MIKRO_ORM_SEEDER_PATH. You are allowed to call the seeder:create command with a name, class name or hyphenated name.
作为示例,我们来看一个非常基本的种子。
¥As an example, let's look at a very basic seeder.
请注意,种子中可用的
EntityManager将启用persistOnCreate(即使你在 ORM 配置中明确禁用它),因此调用em.create()将自动在创建的实体上调用em.persist()。如果你使用实体构造函数,则需要显式调用em.persist()。¥Note that the
EntityManageravailable in seeders will havepersistOnCreateenabled (even if you explicitly disable it in the ORM config), hence callingem.create()will automatically callem.persist()on the created entity. If you use entity constructor instead, you need to callem.persist()explicitly.
import { EntityManager } from '@mikro-orm/core';
import { Seeder } from '@mikro-orm/seeder';
import { Author } from './author'
export class DatabaseSeeder extends Seeder {
async run(em: EntityManager): Promise<void> {
// will get persisted automatically
const author = em.create(Author, {
name: 'John Snow',
email: 'snow@wall.st'
});
// but if you would do `const author = new Author()` instead,
// you would need to call `em.persist(author)` explicitly.
}
}
从命令行或以编程方式运行种子程序将在
run方法完成后自动调用flush和clear。¥Running a seeder from the command line or programmatically will automatically call
flushandclearafter therunmethod has completed.
使用实体工厂
¥Using entity factories
你也可以使用 实体工厂,而不必为每个实体指定所有属性。这些可用于生成大量数据库记录。请阅读 关于如何定义工厂的文档 以了解如何定义你的工厂。
¥Instead of specifying all the attributes for every entity, you can also use entity factories. These can be used to generate large amounts of database records. Please read the documentation on how to define factories to learn how to define your factories.
在下面的示例中,我们生成了 10 个作者实体。
¥In the following example, we generate 10 author entities.
import { EntityManager } from '@mikro-orm/core';
import { Seeder } from '@mikro-orm/seeder';
import { AuthorFactory } from '../factories/author.factory'
export class DatabaseSeeder extends Seeder {
async run(em: EntityManager): Promise<void> {
new AuthorFactory(em).make(10);
}
}
调用其他种子程序
¥Calling additional seeders
在 run 方法内部,你可以指定其他种子类。你可以使用 call 方法将数据库种子分成多个文件,以防止种子文件变得太大。call 方法需要 em 和种子类数组。
¥Inside the run method you can specify other seeder classes. You can use the call method to break up the database seeder into multiple files to prevent a seeder file from becoming too large. The call method requires an em and an array of seeder classes.
import { EntityManager } from '@mikro-orm/core';
import { Seeder } from '@mikro-orm/seeder';
import { AuthorSeeder, BookSeeder } from '../seeders'
export class DatabaseSeeder extends Seeder {
run(em: EntityManager): Promise<void> {
return this.call(em, [
AuthorSeeder,
BookSeeder,
]);
}
}
共享上下文
¥Shared context
你经常需要生成引用其他种子创建的实体的实体。为此,你可以使用第二个参数或 run 方法中提供的共享上下文对象。使用 this.call() 时会自动创建它,并传递给每个种子的 run 方法。让我们看看上一个示例中的 AuthorSeeder 和 BookSeeder 是什么样子:
¥Often you might want to generate entities that are referencing other entities, created by other seeders. For that you can use the shared context object provided in the second parameter or run method. It is automatically created when using this.call() and passed down to each seeder's run method. Let's see how the AuthorSeeder and BookSeeder from previous example could look like:
export class AuthorSeeder extends Seeder {
async run(em: EntityManager, context: Dictionary): Promise<void> {
// save the entity to the context
context.author = em.create(Author, {
name: '...',
email: '...',
});
}
}
export class BookSeeder extends Seeder {
async run(em: EntityManager, context: Dictionary): Promise<void> {
em.create(Book, {
title: '...',
author: context.author, // use the entity from context
});
}
}
实体工厂
¥Entity factories
在测试时,你可以在开始测试之前将实体插入数据库中。你也可以使用 Factory 来使用实体工厂为实体定义一组默认属性,而不必手动指定每个实体的每个属性。
¥When testing you may insert entities in the database before starting a test. Instead of specifying every attribute of every entity by hand, you could also use a Factory to define a set of default attributes for an entity using entity factories.
让我们看一个 作者实体 的示例工厂。
¥Let's have a look at an example factory for an Author entity.
import { Factory } from '@mikro-orm/seeder';
import { faker } from '@faker-js/faker';
import { Author } from './entities/author.entity';
export class AuthorFactory extends Factory<Author> {
model = Author;
definition(): Partial<Author> {
return {
name: faker.person.findName(),
email: faker.internet.email(),
age: faker.random.number(18, 99),
};
}
}
在这里我们扩展了基本 Factory 类,定义了一个 model 属性和一个 definition 方法。model 定义工厂为哪个实体生成实体实例。definition 方法返回使用工厂创建实体时应应用的默认属性值集。
¥Here we extend the base Factory class, define a model property and a definition method. The model defines for which entity the factory generates entity instances. The definition method returns the default set of attribute values that should be applied when creating an entity using the factory.
在此示例中,我们使用一个名为 Faker 的库,它允许你方便地生成各种随机数据以进行测试。
¥In this example, we use a library called Faker, which allows you to conveniently generate various kinds of random data for testing.
自 v6 起,Faker 不再从种子包中重新导出,需要单独安装。
¥Since v6, Faker is no longer re-exported from the seeder package and needs to be installed separately.
使用创建实体工厂
¥Creating entities using factories
定义工厂后,即可使用它们生成实体。导入工厂,实例化它并调用 makeOne 方法:
¥Once you define your factories, you can use them to generate entities. Import the factory, instantiate it and call the makeOne method:
const author = new AuthorFactory(orm.em).makeOne();
生成多个实体
¥Generate multiple entities
通过调用 make 方法生成多个实体。make 方法的参数是你生成的实体数。
¥Generate multiple entities by calling the make method. The parameter of the make method is the number of entities you generate.
// Generate 5 authors
const authors = new AuthorFactory(orm.em).make(5);
覆盖属性
¥Overriding attributes
如果你想覆盖工厂的某些默认值,可以将对象传递给 make 方法。仅替换指定的属性,而其余属性仍设置为工厂指定的默认值。
¥If you would like to override some of the default values of your factories, you may pass an object to the make method. Only the specified attributes will be replaced while the rest of the attributes remain set to their default values as specified by the factory.
// Make a single author
const author = new AuthorFactory(orm.em).makeOne({
name: 'John Snow',
});
// Make 5 authors
const authors = new AuthorFactory(orm.em).make(5, {
name: 'John Snow',
});
持久化实体
¥Persisting entities
create 方法使用 EntityManager 的 persistAndFlush 方法实例化实体并将其持久化到数据库。
¥The create method instantiates entities and persists them to the database using the persistAndFlush method of the EntityManager.
// Make and persist a single author
const author = await new AuthorFactory(orm.em).createOne();
// Make and persist 5 authors
const authors = await new AuthorFactory(orm.em).create(5);
你可以通过将对象传递给 create 方法来覆盖工厂的默认值。
¥You can override the default values of your factories by passing an object to the create method.
// Make and persist a single author
const author = await new AuthorFactory(orm.em).createOne({
name: 'John Snow',
});
// Make and persist a 5 authors
const authors = await new AuthorFactory(orm.em).create(5, {
name: 'John Snow',
});
工厂关系
¥Factory relationships
为一个实体创建大量数据固然很好,但大多数情况下,你需要为多个实体创建数据,并在它们之间建立关系。
¥It is nice to create large quantities of data for one entity, but most of the time you will want to create data for multiple entities and also have relations between them.
通过 .each() 定义关系
¥Defining relations via .each()
为此,你可以使用可以链接在工厂上的 each 方法。可以使用转换工厂输出实体然后再返回的函数来调用 each 方法。让我们看一些不同关系的示例。
¥For that you can use the each method which can be chained on a factory. The each method can be called with a function that transforms output entity from the factory before returning it. Let's look at some examples of the different relations.
多对一和一对一关系
¥ManyToOne and OneToOne relations
const books: Book[] = new BookFactory(orm.em).each(book => {
book.author = new AuthorFactory(orm.em).makeOne();
}).make(5);
一对多和多对多
¥OneToMany and ManyToMany
const books: Book[] = new BookFactory(orm.em).each(book => {
book.owners.set(new OwnerFactory(orm.em).make(5));
}).make(5);
通过 .definition() 定义关系
¥Defining relations via .definition()
或者,你也可以在 definition 方法内部构建嵌套实体。如有需要,此方法可以接受无需包含在实体模式中的附加参数。
¥Alternatively you can build the nested entities inside of the definition method. If needed, this method can accept additional params that need not to be a part of the entity schema.
export class AuthorFactory extends Factory<
AuthorEntity,
EntityData<AuthorEntity> & { booksCount?: number }
> {
model = AuthorEntity;
async definition(
params?: EntityData<AuthorEntity> & { booksCount?: number }
): EntityData<AuthorEntity> {
const name = params.name ?? faker.person.findName();
const books = params.books ?? (
[...Array(params?.booksCount ?? 0)].map((v, i) =>
new BookFactory(this.em).makeEntity({
title: `${name} Trilogy - Part ${i + 1}`
})
)
);
return {
...params,
name,
books
};
}
}
// Finally
new AuthorFactory(em).createOne({ booksCount: 4 })
与 CLI
¥Use with CLI
你可以执行 seeder:run MikroORM CLI 命令来为你的数据库播种。默认情况下,seeder:run 命令运行 DatabaseSeeder 类,而该类又可能调用其他种子类。你可以使用密钥 seeder.defaultSeeder 或使用 环境变量 MIKRO_ORM_SEEDER_DEFAULT_SEEDER 配置默认种子。你还可以使用 --class 选项指定种子类:
¥You may execute the seeder:run MikroORM CLI command to seed your database. By default, the seeder:run command runs the DatabaseSeeder class, which may in turn invoke other seed classes. You can configure the default seeder using the key seeder.defaultSeeder or using the environment variable MIKRO_ORM_SEEDER_DEFAULT_SEEDER. You can also use the --class option to specify a seeder class:
npx mikro-orm seeder:run
npx mikro-orm seeder:run --class=BookSeeder
你还可以使用 migrate:fresh 或 schema:fresh 命令结合 --seed 选项来播种数据库,这将删除所有表并重新运行所有迁移或根据当前实体生成数据库。此命令对于完全重建数据库很有用:
¥You may also seed your database using the migrate:fresh or schema:fresh command in combination with the --seed option, which will drop all tables and re-run all of your migrations or generate the database based on the current entities. This command is useful for completely re-building your database:
npx mikro-orm migration:fresh --seed # will drop the database, run all migrations and the DatabaseSeeder class
npx mikro-orm schema:fresh --seed # will recreate the database and run the DatabaseSeeder class
如果你不想运行 DatabaseSeeder 类,你可以指定默认的种子类应该是什么。这可以通过更改 默认设置 来完成。另一个选项是明确定义要运行的种子类。
¥If you do not want to run the DatabaseSeeder class you can either specify what the default seeder class should be. This can be done by changing the default settings. Another option is to explicitly define the seeder class you want to run.
npx mikro-orm migration:fresh --seed TestSeeder # will drop the database, run all migrations and the TestSeeder class
npx mikro-orm schema:fresh --seed ProjectsSeeder # will recreate the database and run the ProjectsSeeder class
在测试中使用
¥Use in tests
现在你知道了如何创建种子和工厂,但如何在测试中有效地使用它们呢?让我们看以下示例:
¥Now you know how to create seeders and factories, but how can you effectively use them in tests? Let's take a look at the following example:
let orm: MikroORM;
beforeAll(async () => {
// Initialize the ORM
orm = await MikroORM.init({ ... });
// Refresh the database to start clean
await orm.schema.refreshDatabase();
// And run the seeder afterwards
await orm.seeder.seed(DatabaseSeeder);
});
test(() => {
// Do tests
});
afterAll(async () => {
// Close connection
await orm.close();
});
在生产中运行种子
¥Running seeder in production
在生产环境中,你可能需要使用编译后的种子文件。你只需在 ORM 配置中相应地配置种子路径:
¥In a production environment, you might want to use compiled seeder files. All you need to do is to configure the seeder path accordingly in your ORM config:
import { MikroORM, Utils } from '@mikro-orm/core';
await MikroORM.init({
seeder: {
path: 'dist/seeders',
pathTs: 'src/seeders',
},
// or alternatively
// seeder: {
// path: Utils.detectTsNode() ? 'src/seeders' : 'dist/seeders',
// },
// ...
});
这应该允许使用 CLI 生成 TS 种子文件(因为在 CLI 中我们可能已启用 TS 支持),同时在生产中使用编译的 JS 文件,其中 ts-node 未注册。
¥This should allow using CLI to generate TS seeder files (as in CLI we probably have TS support enabled), while using compiled JS files in production, where ts-node is not registered.