作りたい。
factoryboyをご存知ですか?
factory_boy — Factory Boy stable documentation
pythonでテスト実装する際、ダミーデータの実装に役に立つライブラリなのですが、これがまー使いやすい。
ので、prismaでシード作るときにも同じような感じで使いたいなーと思い、実現方法を考えてみました。
factory-botを使ってみる
調べてみると、factory-botというのがありました。
これを使ってみたいと思います。
とりあえずディレクトリを作って初期化します。
mkdir prisma-factory-bot
cd prisma-factory-bot
npm init
次に、typescriptの実行基盤とお目当てのfactory-botをインストールします。
npm i -D typescript ts-node @types/node
npm i factory-bot
これで準備できました。
最も簡易的な形の実装は以下のとおりです。
import { factory } from "factory-bot";
class User {
username: string;
score: number;
constructor(attrs: { username: string, score: number }) {
this.username = attrs.username;
this.score = attrs.score;
}
}
factory.define('user', User, {
username: 'Bob',
score: 50,
});
(async () => {
const user = await factory.build<User>('user');
console.log(user);
})();
とりあえず動く形になりました。
実行すると、生成されたUserが表示されます。
User { username: 'Bob', score: 50 }
bulidMany関数を使うと、複数のモデルをいっぺんに作ることができます。
const users = await factory.buildMany<User>("user", 5);
console.log(users);
いい感じに動きますね〜
Prisma側にスキーマを定義する
ではprismaをインストールして初期化します。
npm i -D prisma
npx prisma init --datasource-provider sqlite # 今回はSQLiteを使用します
次にスキーマを初期化しておきましょう。
ガイドのスキーマをまるっとコピペします。
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
マイグレーションしておきます。
npx prisma migrate dev --name init
これで準備が整いました。
prismaとfactory-botを組み合わせる
思ったよりfactory-botの実装に柔軟性があったので、さっとprismaと組み合わせてseed.ts
を作ります。
その前に一度使えそうなジェネレーターをインストールしておきます。
schema.prisma
のモデルからクラスを生成するジェネレータです。
GitHub - kimjbstar/prisma-class-generator: Class generator from Prisma schema.
npm i prisma-class-generator
schema.prisma
にジェネレータを追加しておきます。
generator prismaClassGenerator {
provider = "prisma-class-generator"
dryRun = false
}
これで準備ができたので、一度生成します。
npx prisma generate
生成したクラスを利用して、factory-botに組み込んでみます。
import { PrismaModel } from './src/_gen/prisma-class';
import { factory } from "factory-bot";
factory.define('user', PrismaModel.User, {
email: factory.seq('User.email', (n) => `user${n}@test.com`),
name: "string",
posts: []
});
(async () => {
const user = await factory.build<PrismaModel.User>('user');
console.log(user);
const users = await factory.buildMany<PrismaModel.User>("user", 5);
console.log(users);
})();
実行します。
% npx ts-node index.ts
/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:859
return new TSError(diagnosticText, diagnosticCodes, diagnostics);
^
TSError: ⨯ Unable to compile TypeScript:
src/_gen/prisma-class/post.ts(2,50): error TS2307: Cannot find module '@nestjs/swagger' or its corresponding type declarations.
src/_gen/prisma-class/post.ts(6,2): error TS2564: Property 'id' has no initializer and is not definitely assigned in the constructor.
src/_gen/prisma-class/post.ts(9,2): error TS2564: Property 'title' has no initializer and is not definitely assigned in the constructor.
src/_gen/prisma-class/post.ts(15,2): error TS2564: Property 'published' has no initializer and is not definitely assigned in the constructor.
at createTSError (/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:859:12)
at reportTSError (/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:863:19)
at getOutput (/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:1077:36)
at Object.compile (/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:1433:41)
at Module.m._compile (/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:1617:30)
at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
at Object.require.extensions.<computed> [as .ts] (/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:1621:12)
at Module.load (node:internal/modules/cjs/loader:1119:32)
at Function.Module._load (node:internal/modules/cjs/loader:960:12)
at Module.require (node:internal/modules/cjs/loader:1143:19) {
diagnosticCodes: [ 2307, 2564, 2564, 2564 ]
エラーになってしまいました。
Swagger関連の実装があるのが邪魔なので、設定を修正して再生成します。
generator prismaClassGenerator {
provider = "prisma-class-generator"
dryRun = false
useSwagger = false # これを追加
}
npx prisma generate
再実行しました。
npx ts-node index.ts
/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:859
return new TSError(diagnosticText, diagnosticCodes, diagnostics);
^
TSError: ⨯ Unable to compile TypeScript:
src/_gen/prisma-class/post.ts(4,2): error TS2564: Property 'id' has no initializer and is not definitely assigned in the constructor.
src/_gen/prisma-class/post.ts(6,2): error TS2564: Property 'title' has no initializer and is not definitely assigned in the constructor.
src/_gen/prisma-class/post.ts(10,2): error TS2564: Property 'published' has no initializer and is not definitely assigned in the constructor.
at createTSError (/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:859:12)
at reportTSError (/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:863:19)
at getOutput (/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:1077:36)
at Object.compile (/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:1433:41)
at Module.m._compile (/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:1617:30)
at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
at Object.require.extensions.<computed> [as .ts] (/home/username/Project/study/prisma-factory-bot/node_modules/ts-node/src/index.ts:1621:12)
at Module.load (node:internal/modules/cjs/loader:1119:32)
at Function.Module._load (node:internal/modules/cjs/loader:960:12)
at Module.require (node:internal/modules/cjs/loader:1143:19) {
diagnosticCodes: [ 2564, 2564, 2564 ]
一つエラーが減りました。
ただ他にもエラーが出ています。
これに関してもいろいろ調べたんですが、オプションを追加することで対処可能でした。
generator prismaClassGenerator {
provider = "prisma-class-generator"
dryRun = false
useSwagger = false
useNonNullableAssertions = true # これを追加
}
ただ、追加してもprisma-class-generator
がコンストラクタを生成してくれないため、作成されたモデルは空になってしまいます。
こんな場合に使えるObjectAdapter
というのがあるので、これを使いましょう。
import { PrismaModel } from './src/_gen/prisma-class';
import { factory } from "factory-bot";
const FactoryBot = require('factory-bot');
const adapter = new FactoryBot.ObjectAdapter();
factory.setAdapter(adapter);
factory.define('user', PrismaModel.User, {
id: factory.seq('User.id', (n) => n),
email: factory.seq('User.email', (n) => `user${n}@test.com`),
name: "string",
posts: [
{
id: factory.seq('Post.id.sub', (n) => n),
title: "string",
published: true,
},
]
});
factory.define('post', PrismaModel.Post, {
id: factory.seq('Post.id', (n) => n),
title: "string",
published: true,
});
(async () => {
const posts = await factory.buildMany<PrismaModel.Post>("post", 5);
console.log(posts);
const users = await factory.buildMany<PrismaModel.User>("user", 5);
console.log(users);
console.log(users[0].posts)
})();
これを実行すると、オブジェクトが生成されていることが確認できます。
% npx ts-node index.ts
[
Post { id: 1, title: 'string', published: true },
Post { id: 2, title: 'string', published: true },
Post { id: 3, title: 'string', published: true },
Post { id: 4, title: 'string', published: true },
Post { id: 5, title: 'string', published: true }
]
[
User {
posts: [ [Object] ],
id: 1,
email: 'user1@test.com',
name: 'string'
},
User {
posts: [ [Object] ],
id: 2,
email: 'user2@test.com',
name: 'string'
},
User {
posts: [ [Object] ],
id: 3,
email: 'user3@test.com',
name: 'string'
},
User {
posts: [ [Object] ],
id: 4,
email: 'user4@test.com',
name: 'string'
},
User {
posts: [ [Object] ],
id: 5,
email: 'user5@test.com',
name: 'string'
}
]
[ { id: 1, title: 'string', published: true } ]
少しいい感じになりましたね。
ではここから更にPrismaでデータ生成もできるようにしていきましょう。
import { PrismaClient } from '@prisma/client';
import { PrismaModel } from './src/_gen/prisma-class';
import { factory } from "factory-bot";
const FactoryBot = require('factory-bot');
class PrismaAdapter extends FactoryBot.ObjectAdapter {
prisma = new PrismaClient();
async save<T extends { new (...args: any[]): any }>(model: any, modelClass: T) {
const className = modelClass.name;
const result = await (this.prisma as any)[className].create({data: {...model}});
return result;
}
async destroy<T extends { new (...args: any[]): any }>(model: any, modelClass: T) {
console.log("destroy")
const className = modelClass.name;
await (this.prisma as any)[className].deleteMany();
return model;
}
}
const adapter = new PrismaAdapter();
factory.setAdapter(adapter);
factory.define('user', PrismaModel.User, {
id: factory.seq('User.id', (n) => n),
email: factory.seq('User.email', (n) => `user${n}@test.com`),
name: "string",
posts: {
create: {
title: "string",
published: true,
},
},
});
factory.define('post', PrismaModel.Post, {
id: factory.seq('Post.id', (n) => n),
title: "string",
published: true,
});
(async () => {
await adapter.prisma.user.deleteMany();
await adapter.prisma.post.deleteMany();
const posts = await factory.createMany<PrismaModel.Post>("post", 5);
console.log(posts);
const users = await factory.createMany<PrismaModel.User>("user", 5);
console.log(users);
})();
adapterという便利な機能があるので、これでPrisma用のAdapterを実装しました。
また、Userの作成はPrismaのリレーション作成機能があるのでそれを使っています。
これをシードとして実行します。
% npx prisma db seed
Environment variables loaded from .env
Running seed command `ts-node prisma/seed.ts` ...
[
{
id: 1,
title: 'string',
content: null,
published: true,
authorId: null
},
{
id: 2,
title: 'string',
content: null,
published: true,
authorId: null
},
{
id: 3,
title: 'string',
content: null,
published: true,
authorId: null
},
{
id: 4,
title: 'string',
content: null,
published: true,
authorId: null
},
{
id: 5,
title: 'string',
content: null,
published: true,
authorId: null
}
]
[
{ id: 1, email: 'user1@test.com', name: 'string' },
{ id: 2, email: 'user2@test.com', name: 'string' },
{ id: 3, email: 'user3@test.com', name: 'string' },
{ id: 4, email: 'user4@test.com', name: 'string' },
{ id: 5, email: 'user5@test.com', name: 'string' }
]
🌱 The seed command has been executed.
データが作成されました!
まとめ
今回はfactory-botで便利なシードを実装しました。
factoryboyにはSubFactoryというのがあって、これを使うと自動で作成までしてくれたりするんですが、そのへんも実装できたら嬉しいですね。。
(Factory実装も独自の実装にしてあげればできそう)
2023-08-24 追記
SubFactory的な実装ありました。
assocというらしいです。
import { PrismaClient, Prisma } from '@prisma/client';
import { PrismaModel } from './src/_gen/prisma-class';
import { factory } from "factory-bot";
const FactoryBot = require('factory-bot');
class FunctionAdapter extends FactoryBot.ObjectAdapter {
async build(func: (args: any) => any, props: any) {
return props;
}
async save(model: any, func: (args: any) => any) {
return await func(model);
}
}
const objectAdapter = new FactoryBot.ObjectAdapter();
const funcAdapter = new FunctionAdapter();
factory.setAdapter(funcAdapter);
const prisma = new PrismaClient();
factory.define("user", prisma.user.create, {
data: {
id: factory.seq('User.id', (n) => n),
email: factory.seq('User.email', (n) => `user${n}@test.com`),
name: "string",
posts: {
create: factory.assocAttrs("post", "data")
},
}
});
factory.define('post', prisma.post.create, {
data: {
id: factory.seq('Post.id', (n) => n),
title: "string",
published: true,
}
});
(async () => {
await prisma.user.deleteMany();
await prisma.post.deleteMany();
const user = await factory.create("user");
console.log(user);
const users = await factory.createMany("user", 5);
console.log(users);
const posts = await factory.createMany<PrismaModel.Post>("post", 5);
console.log(posts);
})();
assocを使用するのと、factory.defineで直接prisma.user.create
関数を使用するようにしました。
assocを使用すると、作成されたモデルを元にして実行し、assocAttrsを使用するとfactory.defineに定義したattrsを参照するようになります。
今回の場合、作成したモデルをそのまま使うとエラーになってしまったためassocAttrsを使用するようにしました。
なかなかいいライブラリだなぁこれ…。