模型工厂
本指南涵盖 Lucid 的模型工厂。你将学会如何:
- 为模型定义工厂并生成实例
- 覆盖默认属性并应用命名状态
- 创建或持久化实例,包括内存和存根变体
- 为关联关系填充数据,包括多对多的中间表属性
- 钩入工厂生命周期事件
- 跨多个连接使用工厂
概述
工厂是一个生成模型逼真实例的小模块。无需在每个测试或种子中手动构建所需字段,你只需声明一次结构并通过覆盖重用。
import User from '#models/user'
import { Factory } from '@adonisjs/lucid/factories'
export const UserFactory = Factory.define(User, ({ faker }) => {
return {
email: faker.internet.email(),
password: faker.internet.password(),
}
}).build()
工厂位于 database/factories/。使用 make:factory Ace 命令生成新工厂。
node ace make:factory User
// CREATE: database/factories/user_factory.ts
Factory.define(Model, callback) 接受模型类和返回默认属性的回调。回调接收运行时上下文,其 faker 属性是一个 Faker.js 实例,用于生成逼真的值。返回包含模型期望的每个必需字段的对象,否则数据库会以 NOT NULL 错误拒绝插入。
在末尾调用 .build() 以完成工厂。返回值是应用程序其余部分导入和使用的内容。
生成模型
工厂可以以三种模式生成模型实例。根据你需要持久化行、无数据库实例还是带模拟主键的内存实例来选择。
create 和 createMany
create() 构建实例,将其持久化到数据库,并返回保存的模型。createMany(count) 对 count 个实例执行相同操作。
import { UserFactory } from '#database/factories/user_factory'
const user = await UserFactory.create()
const users = await UserFactory.createMany(10)
这是你在集成测试和种子中默认使用的方法。
make 和 makeMany
make() 实例化模型而不运行任何数据库查询。返回的实例没有主键,且 $isPersisted 为 false。
const user = await UserFactory.make()
console.log(user.id) // undefined
console.log(user.$isPersisted) // false
当测试只需要练习不触及数据库的模型行为(例如,计算属性、序列化、验证)时使用 make。
makeStubbed 和 makeStubbedMany
makeStubbed() 也跳过数据库但分配内存主键,因此任何期望持久化模型的代码(读取 user.id 的授权助手、缓存键构建器)都可以针对存根工作。
const user = await UserFactory.makeStubbed()
console.log(user.id) // 1, 2, 3, ... 来自全局计数器
console.log(user.$isPersisted) // false
对想要持久化模型外观而不支付真实插入成本的单元测试使用 makeStubbed。
覆盖属性
merge
调用 merge(attributes) 来覆盖此次调用的特定字段。每个其他字段仍然来自工厂的默认回调。
await UserFactory.merge({ email: 'test@example.com' }).create()
生成多个实例时,传入覆盖数组。每个条目应用于相同索引的实例。
await UserFactory
.merge([
{ email: 'foo@example.com' },
{ email: 'bar@example.com' },
])
.createMany(3)
超出数组末尾的实例回退到工厂默认值。在上面的示例中,前两个用户获得显式电子邮件,第三个获得 faker 生成的默认值。
mergeRecursive
mergeRecursive(attributes) 将相同的合并应用于工厂的关联关系,因此覆盖父级上的字段也会覆盖树中每个关联工厂上的字段。
await UserFactory
.with('posts', 2)
.mergeRecursive({ tenantId: 'tenant-a' })
.create()
// tenantId 设置在用户和每篇文章上
当你有一个字段(租户 id、分区键、软删除标志)必须在父级及其后代之间保持一致时使用 mergeRecursive。
pivotAttributes
pivotAttributes 为多对多关联关系设置中间表上的值。参见下面的关联关系。
工厂状态
状态是工厂的命名变体。在 .build() 之前使用 .state(name, callback) 定义它们,并在调用时使用 .apply(...names) 应用。
import Post from '#models/post'
import { Factory } from '@adonisjs/lucid/factories'
export const PostFactory = Factory.define(Post, ({ faker }) => {
return {
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(4),
status: 'DRAFT',
}
})
.state('published', (post) => {
post.status = 'PUBLISHED'
})
.state('archived', (post) => {
post.status = 'ARCHIVED'
post.archivedAt = DateTime.now()
})
.build()
在调用时应用一个或多个状态。
await PostFactory.apply('published').createMany(3)
await PostFactory.apply('published', 'featured').create()
状态回调接收已构建的模型实例加运行时上下文,因此你也可以从那里调用 faker 或读取事务。
使用 tap 进行每实例装饰
tap(callback) 在工厂回调和任何应用的状态之后,为工厂生成的每个实例运行你的回调。用于不值得成为状态的一次性装饰。
const users = await UserFactory
.tap((user, ctx, builder) => {
user.rememberMeToken = generateToken()
})
.createMany(5)
回调接收模型实例、运行时上下文和工厂构建器。就地修改模型;返回值被忽略。
关联关系
工厂与模型关联关系自然组合。使用 .relation(name, factoryFn) 在工厂上声明关联关系,Lucid 从模型检测其类型。
import User from '#models/user'
import { Factory } from '@adonisjs/lucid/factories'
import { PostFactory } from '#database/factories/post_factory'
export const UserFactory = Factory.define(User, ({ faker }) => {
return {
email: faker.internet.email(),
password: faker.internet.password(),
}
})
.relation('posts', () => PostFactory)
.build()
关联关系必须已存在于 Lucid 模型上。.relation 将工厂连接到该关联关系,以便你可以与父级一起创建它。
使用 .with(name, count?, callback?) 在生成父级时创建关联行。
const user = await UserFactory.with('posts', 3).create()
console.log(user.posts.length) // 3
Lucid 将父级和关联写入包装在托管事务中,因此如果关联写入失败,父级插入也会回滚。
对关联行应用状态
传递回调给 .with 以配置关联工厂。回调接收关联工厂,因此你可以链式调用 apply、merge 或进一步嵌套的 with 调用。
const user = await UserFactory
.with('posts', 3, (post) => post.apply('published'))
.create()
在同一个关联关系的多个 with 调用中混合状态。
const user = await UserFactory
.with('posts', 3, (post) => post.apply('published'))
.with('posts', 2)
.create()
console.log(user.posts.length) // 5
嵌套关联关系通过回调内的链式 .with 工作。
const user = await UserFactory
.with('posts', 2, (post) => post.with('comments', 5))
.create()
多对多的中间表属性
对于多对多关联关系,.pivotAttributes(attrs) 设置一起插入的中间表行上的列。
await UserFactory
.with('teams', 1, (team) => {
team.pivotAttributes({ role: 'admin' })
})
.create()
传入数组以为每行设置不同的中间表属性。数组长度必须与关联行数匹配。
await UserFactory
.with('teams', 2, (team) => {
team.pivotAttributes([
{ role: 'admin' },
{ role: 'moderator' },
])
})
.create()
钩子
注册回调以在工厂生命周期事件之前或之后运行。钩子在 .build() 之前在工厂模型上声明。
Factory.define(Post, () => ({ /* ... */ }))
.before('create', (factory, model, ctx) => {
// 在 INSERT 之前运行
})
.after('create', (factory, model, ctx) => {
// 在 INSERT 之后运行
})
.build()
钩子回调接收工厂构建器、模型实例和运行时上下文。
| 钩子 | 触发时机 |
|---|---|
before('create') | 当使用 create/createMany 时在 INSERT 之前 |
after('create') | 在 INSERT 之后 |
before('makeStubbed') | 在存根实例上分配主键之前 |
after('makeStubbed') | 在存根实例准备好之后 |
after('make') | 在实例构建之后,适用于每种模式(make、makeStubbed、create) |
你可以为同一事件注册多个钩子。它们按注册顺序运行。
运行时上下文
为每次工厂调用创建运行时上下文,并传递给属性回调、状态回调、关联关系、钩子和 tap。
Factory.define(User, (ctx) => {
return { /* ... */ }
})
.state('admin', (model, ctx) => { /* ... */ })
.before('create', (factory, model, ctx) => { /* ... */ })
.after('create', (factory, model, ctx) => { /* ... */ })
.build()
上下文暴露以下字段:
faker,用于生成值的 Faker.js 实例isStubbed,指示工厂是否使用makeStubbed调用的布尔值$trx,包装工厂写入的事务(如果存在)
通过 ctx.$trx 上的事务在钩子内运行任何数据库查询,以便它们与工厂自己的写入一起提交或回滚。
跨连接使用工厂
工厂默认继承模型的连接。当你的测试或种子在非默认连接上操作时,按调用覆盖。
// 通过连接名称
await UserFactory.connection('tenant-a').create()
// 使用显式查询客户端
import db from '@adonisjs/lucid/services/db'
await UserFactory.client(db.connection('tenant-a')).create()
// 通过 query() 快捷方式,接受任一种
await UserFactory.query({ connection: 'tenant-a' }).create()
覆盖应用于当前工厂链。后续工厂调用仍然使用默认值,除非它们设置自己的覆盖。
自定义实例化
newUp
newUp(callback) 替换 Lucid 的默认实例化。当模型需要非标准构造步骤时使用。
Factory.define(User, () => ({ /* ... */ }))
.newUp((attributes, ctx) => {
const user = new User()
user.fill(attributes)
return user
})
.build()
merge
merge(callback) 替换默认的属性合并行为。回调决定如何将属性对象应用于模型实例。
Factory.define(User, () => ({ /* ... */ }))
.merge((user, attributes, ctx) => {
user.merge(attributes)
})
.build()
自定义存根主键
默认情况下,makeStubbed 为模型的主键分配递增整数。当你的模型使用非整数主键(UUID、BigInt、ULID)时,使用 Factory.stubId 全局覆盖。
import { Factory } from '@adonisjs/lucid/factories'
Factory.stubId((counter, model) => {
return BigInt(counter)
})
对于每个工厂的自定义,注册 before('makeStubbed') 钩子,在 Lucid 的存根运行之前分配主键。
import { randomUUID } from 'node:crypto'
Factory.define(Post, () => ({ /* ... */ }))
.before('makeStubbed', (_, model) => {
model.id = randomUUID()
})
.build()