作りたい。
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を使用するようにしました。
なかなかいいライブラリだなぁこれ…。