Closed1

Next.js, Prisma でアプリ作る時のseedデータ作成にこまった

山本隼汰山本隼汰

前提

  • TypeScript, Next.js (AppRouter), Prisma でプロダクトを作ってる時の話
  • PostgreSQL をデータベースとして使用しており、prisma migrate コマンドでマイグレーションをかける想定

結論

  • 以下の記事を参考にし tsconfig.local.json を作成する方法を使った
    • package.json の module を commonjs に書き換えて、的な話を書いている記事は多いが、Next.js は esnext を推奨しているため、闇雲に tsconfig.local.json を書き換えない方が良い
    • seed データを作成する時のみ、--project tsconig.local.json で config を渡すようにする

https://zenn.dev/damono999/articles/bd203095b7b883

詳細

事前に ts-node をインストールしておく必要があります

pnpm add -D ts-node

最終的に、ディレクトリはこう言う感じ。

app/
...
prisma/
    migrations/
    seeds/
        0_users.ts # prisma 使ってデータ作る処理
        1_books.ts
        main.ts # seed データを生成する処理
    schema.prisma
tsconfig.json # 通常使うもの
tsconfig.local.json # シードデータ作成時のみ使うもの
Makefile # タスクランナー(と言うテイ)
package.json
pnpm-lock.yaml

tsconfig.local.json はご希望のものがあれば0から作っても良いが、特にこだわりがなければ

cp tsconfig.json tsconfig.local.json

としてしまえば良い。そうした後に

tsconfig.local.json
{
    "compilerOptions": {
        ...,
-        "module": "nextes",
+        "module": "commonjs",
    }
}

package.json にはこういう感じで書く

package.json
{
    "name": "test",
    "private": true,
    "version": "0.1.0",
    "scripts": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start",
        "lint": "next lint"
    },
+    "prisma": {
+        "seed": "ts-node --project tsconfig.local.json prisma/seeds/main.ts"
+    }
}

0_users.ts の中身はこう言う感じで書いてます。${number}_${table} としてるのは、テーブルを作成した順番の方がアルファベットよりも意識を向けることが多いためそうしてます。migrations とだいたい気持ちは一緒です

0_users.ts
import type { PrismaClient } from "@prisma/client";

const email = ""; // メールアドレス

const seedUsers = async (prisma: PrismaClient) => {
    const start = Date.now();
    console.log("Seeding users...");

    const user = await prisma.user.upsert({
        where: {
            email,
        },
        update: {},
        create: {
            email,
            name: "Test User",
        },
    });

    const end = Date.now();
    console.log(`Seeding users completed in ${end - start}ms`);
    
    return user;
};

export default seedUsers;

main.ts でこう言う感じで使う。同じ階層に書いとくことで、import できない云々の問題を避けれるようにした

main.ts
import { PrismaClient } from "@prisma/client";
import seedUsers from "./0_users";
import seedBooks from "./1_books";

const prisma = new PrismaClient();

async function main() {
    const start = new Date();
    console.log("Seeding database...");
    
    const user = await seedUsers(prisma);
    const books = await seedBooks(prisma, user.id);
        
    const end = new Date();
    console.log(`Seeding completed: ${end.getTime() - start.getTime()}ms`);
}

main()
    .then(async () => {
        await prisma.$disconnect();
    })
    .catch(async (e) => {
        console.error(e);
        await prisma.$disconnect();
        process.exit(1);
    });

実際は、テーブルが複数あるので依存関係に応じて簡素なパイプラインを組んでます。DAG のライブラリとか、Pipeline ツールを使っても良いです。ただ、そこまでしなくても Promise でそれなりにできる(と思った)

例えば、(言語違いますが)Python の Apache Airflow の記法で疑似コードを書くとこんなイメージのデータがあるとして、簡素なパイプラインは作成処理はこういう感じになります

users >> books

(users, books) >> X
(X, users) >> XX
(X, users) >> XXX

books >> Y
(users, Y) >> YY
(users, Y) >> YYY
    const user = await seedUsers(prisma);
    const books = await seedBooks(prisma, user.id);

    await Promise.all([
        seedX(prisma, user.id, books[0].id).then(async (x) => {
            await Promise.all([
                seedXX(prisma, user.id, x.id);
                seedXXX(prisma, user.id, x.id);    
            ]);
        },
        seedY(prisma, books[0].id).then(async (y) => {
            await Promise.all([
                seedYY(prisma, user.id, y.id);
                seedYYY(prisma, user.id, y.id);
            ]);
        })
    ]);

で、お好みのタスクランナーで Prisma の実行タイミングでデータベースの情報を引き渡してテストデータを生成してください。私の場合は、Makefile に以下のような定義をして make prisma-seed-dev としています。(ツール選定に理由はありません。ただ慣れているからだけ)

prisma-seed-dev: ## execute prisma db seed
    dotenv -e .env -- npx prisma db seed
このスクラップは2ヶ月前にクローズされました