•11 min read

PrismaとTypeScriptを用いたデータベースシーディング戦略

PrismaとTypeScriptを用いたデータベースシーディング戦略

データベースのシーディングは、往々にして後回しにされがちです。開発環境に少量のモックデータを投入するための、急ごしらえのスクリプトとして扱われることが多いでしょう。しかし、アプリケーションがスケールし、ドメインモデルがますます複雑になるにつれて、脆いシードスクリプトは開発者の生産性や自動テストにとって重大なボトルネックとなります。

現代のNode.jsエコシステムにおいて、PrismaとTypeScriptを組み合わせることで、堅牢で型安全なデータベースシーディングツールを構築するための非常に強力な基盤が得られます。このガイドでは、一般的なチュートリアルをはるかに超える、データベースシーディングの高度な戦略を探求します。プログラムによるシーディング、現実的なモックデータのためのFaker.jsの統合、そして何よりも、深くネストされたリレーショナルデータをエレガントに処理する方法に焦点を当てます。

Audio Briefing
0:00 / 0:00

基本的なシーディングが失敗する理由

典型的なPrismaの入門チュートリアルでは、一連のprisma.user.create()呼び出しを実行するシンプルなスクリプトの記述を推奨しています。これは初日には機能しますが、10日目には破綻します。基本的なスクリプトには、いくつかの重大な欠陥があります。

  1. 冪等性の欠如: スクリプトを2回実行すると、一意制約違反が発生します。
  2. ハードコードされたデータ: 静的に定義されたデータは、ページネーション、検索機能、または複雑なUI状態をテストする能力を制限します。
  3. リレーショナルスパゲッティ: 何十ものテーブルにわたる外部キーを手動で管理するのはエラーが発生しやすいです。
  4. パフォーマンスの問題: 数千の個別の挿入操作を順次待機すると、CIパイプラインが劇的に遅くなります。

プロフェッショナルグレードのシーディング戦略を構築するには、これらの課題のそれぞれに体系的に対処する必要があります。

Advertisement

冪等性とデータベースのクレンジング

シードスクリプトは冪等である必要があります。データベースの状態を壊したり、エラーに遭遇したりすることなく、繰り返し実行できるべきです。これを実現するには、一般的に2つのアプローチがあります。積極的なクレンジングと慎重なアップサートです。

戦略1: 積極的なクレンジング

シードするたびに完全に新しい状態が必要な場合は、データベースをワイプする必要があります。Prismaでは、外部キー制約を尊重して正しい順序でレコードを削除するのは面倒な場合があります。各テーブルから手動で削除する代わりに、テーブルを動的にtruncateできます。

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function cleanDatabase() {
  const tableNames = await prisma.$queryRaw<
    Array<{ tablename: string }>
  >`SELECT tablename FROM pg_tables WHERE schemaname='public'`;

  const tables = tableNames
    .map(({ tablename }) => tablename)
    .filter((name) => name !== '_prisma_migrations')
    .map((name) => `"public"."${name}"`)
    .join(', ');

  try {
    await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${tables} CASCADE;`);
    console.log('Database cleaned successfully.');
  } catch (error) {
    console.error('Error cleaning database', error);
  }
}

注: 上記のスニペットはPostgreSQL構文を使用しています。アプローチは基盤となるデータベースエンジンによって異なります。

戦略2: コアデータのためのアップサート

存在しなければならないコア設定、ロール、またはタクソノミーについては、upsertが最良の友です。upsertは、レコードが既に存在する場合でもエラーをスローすることなく、レコードの存在を保証します。

async function seedRoles() {
  const roles = ['ADMIN', 'USER', 'EDITOR'];

  for (const roleName of roles) {
    await prisma.role.upsert({
      where: { name: roleName },
      update: {},
      create: { name: roleName },
    });
  }
}

モックデータのためのtruncateと、基盤となるデータのためのアップサートを組み合わせることで、堅牢で再現性のあるシーディングのベースラインを作成できます。

現実的なデータのためのFaker.jsの統合

現実世界の利用をシミュレートするには、かなりの量の現実的なデータが必要です。@faker-js/fakerはこれに関する業界標準です。

Fakerをシードスクリプトに統合する際に最も重要(かつ見落とされがち)なステップは、決定論的なシードを設定することです。これにより、チームのすべての開発者とCI環境が、まったく同じ「ランダムな」データを生成することが保証されます。

import { faker } from '@faker-js/faker';

// Set a deterministic seed for consistent generation
faker.seed(12345);

function createUserFactory() {
  const firstName = faker.person.firstName();
  const lastName = faker.person.lastName();
  
  return {
    email: faker.internet.email({ firstName, lastName }).toLowerCase(),
    name: `${firstName} ${lastName}`,
    bio: faker.lorem.paragraph(),
    avatarUrl: faker.image.avatar(),
  };
}

ファクトリ関数を使用すると、メインのシーディングロジックをクリーンに保ち、データの配列を簡単に生成できます。

リレーショナルデータの習得

データベースシーディングの真の複雑さは、リレーションシップの管理にあります。Prismaのネストされた書き込みは、単一のトランザクションで関連データのグラフを作成するためのエレガントなソリューションを提供します。

ディープライト

ユーザーを作成し、そのIDを取得し、そのIDにマッピングされた投稿を作成する代わりに、すべてを一度に行うことができます。

async function seedUserWithPosts() {
  await prisma.user.create({
    data: {
      ...createUserFactory(),
      posts: {
        create: Array.from({ length: 5 }).map(() => ({
          title: faker.lorem.sentence(),
          content: faker.lorem.paragraphs(3),
          published: faker.datatype.boolean(),
        })),
      },
    },
  });
}

このアプローチは非常に読みやすく、手動でのID追跡なしに参照整合性を保証します。

複雑なリレーションシップの解決

多対多のリレーションシップや、動的に生成されたレコードへのリレーションシップがある場合はどうでしょうか?たとえば、投稿にランダムなタグを割り当てる場合などです。

これを処理するには、まず関連エンティティのプールを生成し、作成フェーズでそれらからランダムに選択する必要があります。

async function seedComplexGraph() {
  // 1. Create a pool of tags
  const tagsData = Array.from({ length: 10 }).map(() => ({
    name: faker.word.noun(),
  }));
  
  await prisma.tag.createMany({ data: tagsData });
  const allTags = await prisma.tag.findMany();

  // 2. Helper to get random tags
  const getRandomTags = (count: number) => {
    return faker.helpers.arrayElements(allTags, count).map(tag => ({
      id: tag.id
    }));
  };

  // 3. Create posts and connect random tags
  await prisma.post.create({
    data: {
      title: faker.lorem.sentence(),
      content: faker.lorem.paragraphs(),
      tags: {
        connect: getRandomTags(3),
      },
      author: {
        create: createUserFactory(),
      }
    }
  });
}

connect構文を使用すると、新しく作成されたレコードを既存のレコードに簡単にリンクできます。

Advertisement

パフォーマンス: バッチ処理とトランザクション

シードスクリプトをスケーリングして数万のレコードを生成する場合、個々のprisma.model.create()呼び出しを待機すると、ネットワークオーバーヘッドとデータベースのラウンドトリップにより、深刻なパフォーマンス低下が発生します。

最適化するには、チャンク処理と組み合わせたcreateManyを利用します。

async function seedLargeVolumeOfUsers() {
  const totalUsers = 10000;
  const batchSize = 1000;
  
  for (let i = 0; i < totalUsers; i += batchSize) {
    const usersBatch = Array.from({ length: batchSize }).map(createUserFactory);
    
    await prisma.user.createMany({
      data: usersBatch,
      skipDuplicates: true,
    });
    
    console.log(`Seeded batch ${i / batchSize + 1}`);
  }
}

createManyは、配列全体に対して単一のINSERTステートメントを実行し、プロセスを桁違いに高速化します。

createManyを使用できない複雑なリレーショナルデータがある場合(createManyはネストされたリレーションをサポートしないため)、Prismaトランザクション(prisma.$transaction)にフォールバックして、複数のcreate呼び出しを単一のデータベーストランザクションにグループ化し、コミットオーバーヘッドを大幅に削減できます。

シードディレクトリの構造化

シードロジックが成長するにつれて、単一のseed.tsファイルは管理不能になります。ファクトリとランナーのパターンを採用しましょう。

prisma/
  seed/
    index.ts        # The main entry point orchestrator
    factories/
      user.ts       # User factory functions
      post.ts       # Post factory functions
    runners/
      roles.ts      # Logic to seed foundational roles
      mockData.ts   # Logic to orchestrate Faker data

あなたのindex.tsは単にランナーを調整します。

import { PrismaClient } from '@prisma/client';
import { seedRoles } from './runners/roles';
import { seedMockData } from './runners/mockData';
import { cleanDatabase } from './utils/clean';

const prisma = new PrismaClient();

async function main() {
  console.log('Starting seed process...');
  
  if (process.env.NODE_ENV !== 'production') {
    await cleanDatabase();
  }
  
  await seedRoles(prisma);
  
  if (process.env.NODE_ENV === 'development') {
    await seedMockData(prisma);
  }
  
  console.log('Seed completed.');
}

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

結論

データベースシーディングは単なる雑用ではありません。堅牢なエンジニアリング文化の重要な要素です。TypeScriptの型安全性、Prismaの表現力豊かなORM構文、そしてFaker.jsを活用することで、チームの足を引っ張るのではなく、力を与えるシーディングインフラストラクチャを構築できます。

冪等性を受け入れ、リレーションシップにはネストされた書き込みを利用し、バッチ処理で最適化しましょう。未来のあなた自身、そしてあなたのQAチームは感謝するでしょう。

こちらもおすすめ

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement