模型工厂

未匹配的标注

你是否编写过测试,其中每个测试的前 15-20 行 专门用于通过使用多个模型来设置数据库状态?使用模型工厂,你可以将所有这些设置提取到一个专用文件中,然后编写最少的代码来设置数据库状态。

在本指南结束时,你将了解:

  • 如何创建和使用工厂
  • 如何定义工厂状态
  • 处理模型关系
  • 使用 faker API 生成和使用随机数据

创建工厂

模型工厂存储在 databases/factories 目录中。你可以在单个文件中定义所有工厂或为每个模型创建专用文件,选择权在你手中。

注:你可以使用 make:factory 命令创建一个新工厂。该命令接受你要为其创建工厂的模型名称。

与播种机或模型不同,工厂本质上是声明性的,如下例所示:

// title: database/factories/index.ts
import User from 'App/Models/User'
import Factory from '@ioc:Adonis/Lucid/Factory'

export const UserFactory = Factory
  .define(User, ({ faker }) => {
    return {
      username: faker.internet.userName(),
      email: faker.internet.email(),
      password: faker.internet.password(),
    }
  })
  .build()
  • Factory.define 方法总共接受两个参数。
  • 第一个参数是对 Lucid 模型的引用。
  • 第二个参数是一个回调,它返回在持久化模型实例时要使用的属性对象。确保返回具有所有必需属性的对象,否则数据库将引发not null异常。
  • 最后,确保调用build方法。

使用工厂

使用工厂非常简单, 只需 import 文件并使用其导出的工厂。

import { UserFactory } from 'Database/factories'

const user = await UserFactory.create()

为了创建多个实例,你可以使用 createMany 方法。

const users = await UserFactory.createMany(10)

合并属性

你可以使用 .merge 方法合并默认属性集合,如:

await UserFactory
  .merge({ email: 'test@example.com' })
  .create()

当创建多个实例时,你可以定义一个属性数组,它将根据索引进行合并,如:

await UserFactory
  .merge([
    { email: 'foo@example.com' },
    { email: 'bar@example.com' },
  ])
  .createMany(3)

在上面的例子中:

  • 第一个用户将是 foo@example.com 的邮件地址
  • 第二个用户将是 bar@example.com 的邮件地址
  • 由于合并的数组长度为2,第三个用户将使用默认的邮件地址

工厂状态

工厂状态允许你将工厂的变体定义为状态。如:在 Post 工厂中,你可以用不同的状态来表示已经发布的贴子和草稿

import Factory from '@ioc:Adonis/Lucid/Factory'
import Post from 'App/Models/Post'

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') // 👈
  .build()

默认情况下,所有帖子都会以 DRAFT 状态创建,但你也可以显式指定 published 状态来创建具有 PUBLISHED 状态的帖子。

await PostFactory.apply('published').createMany(3)
await PostFactory.createMany(3)

关系

模型工厂使处理关系变得超级简单。考虑以下示例:

export const PostFactory = Factory
  .define(Post, ({ faker }) => {
    return {
      title: faker.lorem.sentence(),
      content: faker.lorem.paragraphs(4),
      status: 'DRAFT',
    }
  })
  .build()

export const UserFactory = Factory
  .define(User, ({ faker }) => {
    return {
      username: faker.internet.userName(),
      email: faker.internet.email(),
      password: faker.internet.password(),
    }
  })
  .relation('posts', () => PostFactory) // 👈
  .build()

现在,你可以在一次调用中同时创建一个 user 及其 posts

const user = await UserFactory.with('posts', 3).create()
user.posts.length // 3

注意事项

  • 工厂将通过检查 Lucid 模型找到关系类型。例如:如果你的模型在 posts 上定义了 hasMany 关系,那么工厂将推断出相同的关系。
  • 首先需要在模型上定义关系,然后才能在工厂上定义它。
  • Lucid 将在内部将所有数据库操作包装在事务中。因此,如果关系不持续存在,父模型也将回滚。

应用关系状态

你还可以通过将回调传递给 with 方法来对关系应用状态。

const user = await UserFactory
  .with('posts', 3, (post) => post.apply('published'))
  .create()

同样,如果你愿意,可以创建一些带有 published 状态的帖子,而没有它的帖子很少。

const user = await UserFactory
  .with('posts', 3, (post) => post.apply('published'))
  .with('posts', 2)
  .create()

user.posts.length // 5

最后,你还可以创建嵌套关系。例如:创建一个具有两个帖子每个帖子五个评论的用户。

const user = await UserFactory
  .with('posts', 2, (post) => post.with('comments', 5))
  .create()

透视属性

创建 多对多 关系时,你可以使用 pivotAttributes 方法定义数据透视表的属性。

在以下示例中,User 模型与 Team 模型具有多对多关系,我们在指定团队中定义用户角色。

await UserFactory
  .with('teams', 1, (team) => {
    team.pivotAttributes({ role: 'admin' })
  })
  .create()

在创建关系的多个实例时,你可以将对象数组传递给 pivotAttributes 方法。

注:数组的大小应与你即将创建的关系行数相匹配。

await UserFactory
  .with('teams', 2, (team) => {
    team.pivotAttributes([
      { role: 'admin' },
      { role: 'moderator' }
    ])
  })
  .create()

存根数据库调用

在某些情况下,你可能更喜欢将数据库调用存根,只想创建内存中的模型实例。这可以使用 makeStubbedmakeStubbedMany 方法来实现。

const user = await UserFactory
  .with('posts', 2)
  .makeStubbed()

console.log(user.id) // <some-id>
console.log(user.$isPersisted) // false

存根调用永远不会命中数据库,并将分配一个内存中的数字 id 给模型实例。

自定义存根 id

备注:
当我们说 id 时,指的是模型的主键,而不是固定的命名属性id

存根 id 只是一个内存中的计数器,它会随着每次调用而不断增加。如果需要,你可以自定义方法以不同方式生成存根 ID。

例如:在使用 PostgreSQL bigInteger 数据类型时将 ids 生成为 BigInt

import Factory from '@ioc:Adonis/Lucid/Factory'

Factory.stubId((counter, model) => {
  return BigInt(counter)
})

你可以使用 makeStubbed 挂钩来自定义单个工厂的 id 生成行为。

Factory
  .define(Post, () => {
    return {}
  })
  .before('makeStubbed', (_, model) => {
    model.id = uuid.v4()
  })

运行时上下文

每次从工厂创建模型实例时,也会同时创建运行时上下文。然后将上下文传递给所有钩子、define 方法回调以及关系。

大多数时候,你只想从上下文中访问 faker 对象。以下是可用的属性:

  • isStubbed:一个布尔值,知道工厂是否在存根模式下实例化。
  • $trx:一个事务对象,所有的数据库操作都封装在它之下。如果你在工厂挂钩内运行任何数据库查询,请确保也将它们包装在事务中。

以下示例展示了接收运行时上下文 (ctx) 的回调。

Factory
  .define(User, (ctx) => {
  })
  .before('create', (factory, model, ctx) => {
  })
  .after('create', (factory, model, ctx) => {
  })
  .state('admin', (model, ctx) => {
  })
  .build()

钩子

工厂公开了以下钩子来对某些事件发生之前或之后执行操作,你还可以为单个事件定义多个挂钩。

Factory
  .define(Post, () => {})
  .before('create', () => {})
  .after('create', () => {})
生命周期 事件 描述
before create 在插入查询之前调用
after create 在插入查询之后调用
before makeStubbed 在存根调用之前调用
after makeStubbed 在存根调用之后调用
after make 仅在创建模型实例之后调用,这个钩子也在 before createbefore makeStubbed 钩子之前被调用

自定义连接

工厂允许你在使用它们时定义自定义连接或查询客户端。例如:

await Factory.connection('tenant-1').create()

你也可以传递自定义查询客户端实例。

const queryClient = Database.connection('tenant-1')
await Factory.client(queryClient).create()

为了工厂和 Lucid 模型之间的 API 统一,你还可以使用 query 方法定义 connectionclient

await Factory.query({ connection: 'tenant-1' }).create()

自定义

最后,你可以选择自定义在后台执行的某些操作的行为。

newUp

通过定义 newUp 处理程序,可以自定义为给定工厂实例化模型实例的过程。

Factory
  .define(User, () => {

  })
  .newUp((attributes, ctx) => {
    const user = new User()
    user.fill(attributes)

    return user
  })
  .build()

merge

通过定义 merge 处理程序,可以自定义合并行为。

Factory
  .define(User, () => {

  })
  .merge((user, attributes, ctx) => {
    user.merge(attributes)
  })
  .build()

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/adonisjs/5.x/mo...

译文地址:https://learnku.com/docs/adonisjs/5.x/mo...

上一篇 下一篇
贡献者:2
讨论数量: 0
发起讨论 只看当前版本


暂无话题~