💨

スキーマからリアルなテストデータを生成する zero-config な DB seeder を作った

に公開

はじめに

バックエンド開発をしていると、毎回同じ壁にぶつかります。本番には数百万行あるのに、開発用 DB には 2 行しか入っていない。ページネーションの 100 ページ目をテストできない。EXPLAIN ANALYZE を 50 行に対して打っても何も分からない。

この問題への定番の対処は、factory コードを書く・INSERT をスクリプトで流す・fixture ファイルを管理する、のいずれかです。しかしどれもスキーマが変わった瞬間に腐ります。カラムを 1 本足すたびに factory を直して回るのは、本質的な作業ではありません。

そこで seeder という CLI ツールを作りました。DSN を渡すだけで、スキーマから推論してリアルなダミーデータを流し込みます。

https://github.com/mickamy/seeder

seeder とは

ひとことで言うと「zero-config な DB seeder」です。設定ファイルも factory コードも要りません。MySQL / PostgreSQL の DSN を渡すと、

  1. テーブル・カラム・主キー・外部キー・enum をイントロスペクトする
  2. 各カラムについて、名前と SQL 型から「それっぽいジェネレータ」を推論する
  3. 外部キー依存をトポロジカルソートして、親 → 子の順に
  4. バルクインサート (MySQL は multi-row INSERT、Postgres は COPY) で流し込む

という流れで動きます。

$ seeder postgres://user:pass@localhost:5432/mydb --rows 1000 --truncate --seed 42
seeder: 3 table(s), 3 FK(s)
order:  users -> orders -> comments
mode:   truncate + insert
  users     1000 rows (6.7ms)
  orders    1000 rows (7.0ms)
  comments  1000 rows (9.9ms)
done:   3000 row(s) in 53ms

インストール

Homebrew が一番手軽です。

brew install mickamy/tap/seeder

Go が入っていれば go install でも入ります (ビルドには Go 1.26+ が必要)。

go install github.com/mickamy/seeder@latest

Windows 向けには Releases に zip があります。macOS / Linux / Windows × amd64 / arm64 のビルド済みバイナリがタグごとに公開されます。

使い方

基本は DSN を渡すだけです。

seeder postgres://user:pass@localhost:5432/mydb
seeder mysql://root:pass@localhost:3306/mydb

よく使うフラグ:

フラグ 説明
--rows N テーブルあたりの行数 (default 1000)
--truncate INSERT 前に TRUNCATE する (default は append)
--seed N 乱数シードを固定して再現可能にする
--locale ja 名前・住所などを日本語ロケールにする
--dry-run プランだけ表示して INSERT しない
--verbose カラムごとの推論結果を表示する
--exclude t1,t2 指定テーブルをスキップ
--tables t1,t2 指定テーブルだけ対象にする

--verbose を付けると、どのカラムがどのルールにマッチしたかが分かります。

$ seeder $DATABASE_URL --rows 5 --verbose
seeder: 3 table(s), 3 FK(s)
order:  users -> posts -> comments
mode:   dry-run (no INSERT)
  users
    id          skip: serial default
    email       unique-aware name match: Email
    name        name match: Name
    role        kind: string
    active      name match: Bool
    created_at  name match: PastDate
  posts
    id          skip: serial default
    user_id     fk: users.id
    title       name match: LoremIpsumSentence
    body        name match: LoremIpsumParagraph
    tags        kind: json
    view_count  kind: int
    published   kind: bool
    created_at  name match: PastDate

仕組み

スキーマのイントロスペクト

information_schema (Postgres では enum 型や複合 FK の列順のために pg_catalog / pg_constraint も) を読んで、テーブル・カラム・主キー・外部キー・enum ラベルを取得します。読み取りは標準の SELECT のみ。スキーマ変更も特権アクセスも要りません。

カラムの推論

各カラムはまず名前パターンにマッチさせ、外れたら SQL 型でフォールバックします。

パターン ジェネレータ
email, *_email メールアドレス
name, first_name, ... 人名
phone, tel, mobile 電話番号
*_url, link, homepage URL
created_at, updated_at, *_at 過去 1 年のタイムスタンプ
price, amount, *_yen 金額レンジの int
is_*, has_*, *_flag boolean
Postgres enum ラベルからランダム
それ以外 推論した Kind でフォールバック

--locale ja を付けると、人名・住所・都道府県・電話番号・郵便番号などが日本語の辞書に切り替わります。email*_url のようなロケール非依存のものは英語のままです。

DB に任せるカラム

次の 2 つだけは seeder が値を生成せず、DB に任せます。

  • IDENTITY カラム (Postgres の GENERATED ALWAYS AS IDENTITY / MySQL の AUTO_INCREMENT)
  • デフォルトが Postgres のシーケンス呼び出し (DEFAULT nextval(...)) のもの。これは serial / bigserial が展開された形です

逆に、それ以外は DEFAULT が付いていても seeder が値を生成して上書きします。status text DEFAULT 'active' のような列を全行同じ値にしてしまうとテストデータとして役に立たないからです。

UNIQUE 制約への配慮

単一カラムの UNIQUE 制約が付いた列は、衝突を避けるジェネレータにルーティングされます。email なら <uuid>@example.com、その他の文字列は名前ルールに UUID サフィックスを足し、整数列はずっと広いレンジに広げます。

外部キーの解決

テーブルは依存順 (親 → 子) に挿入され、子は各 FK 列について実在する親の PK をランダムに選びます。

  • 複合 FK: 親行を一度選んでから参照列を割り当てるので、別々の親の列を混ぜた不整合な行になりません
  • ポリモーフィック関連 (Rails 風の *_type + *_id): information_schema からは検出できないので seeder.yaml で宣言します
  • 自己参照 FK (employees.manager_id → employees.id): バッチ内の前方参照を解決します
  • 本物の循環 (A → B → A): 満たせる順序が無いのでエラーで報告します

seeder.yaml で細かく制御する

スキーマだけでは決められないものは、seeder.yaml を置くと自動検出されます。CLI フラグ > seeder.yaml > ビルトインのデフォルト、の優先順です。

rows: 1000
seed: 42
locale: en
truncate: false
tables:
  users:
    rows: 5000
    columns:
      email:
        generator: Email
      bio:
        value: dogfood seed row
  orders:
    rows: 10000
  comments:
    exclude: true

カラム単位の上書きは generator (ビルトインを強制) か value (リテラルを固定) のどちらか一方を指定します。ポリモーフィック関連はこう書きます。

tables:
  comments:
    polymorphic:
      - type_col: commentable_type
        id_col: commentable_id
        targets:
          - { table: posts,    type: Post }
          - { table: articles, type: Article }

seeder.yaml はロード時に検証され、タイプミス (例: truncates:) は行番号付きで弾かれます。

その他の出力モード

DB に挿入する以外の使い道もあります。

  • --output sql: INSERT 文を吐き出す。migration リポジトリに seed.sql をコミットしたいとき向け
  • --output ndjson: 1 行 1 JSON で _table 付き。ETL / streaming パイプラインに流すとき向け
  • --stream --rate N: 初回シード後、毎秒およそ N 行を追記し続ける。CDC やレプリケーション遅延のテスト向け
  • --cache <file>: イントロスペクト結果をキャッシュして再実行を高速化

技術的な話

  • 言語: Go 1.26、単一の静的バイナリ
  • DB ドライバ: jackc/pgx/v5 (Postgres は CopyFrom で高速パス) と go-sql-driver/mysql
  • 値生成: brianvoe/gofakeit/v7
  • FK 順序づけ: トポロジカルソートを自前実装
  • リリース: goreleaser で各 OS / arch のバイナリと Homebrew formula を自動公開

大量行でもメモリが膨らまないよう、--batch-size (default 1000) ごとに生成 → フラッシュを繰り返します。FK 解決用の親 PK プールも (テーブル, 列) あたり 10 万件で上限を設けています。

ちなみに json / jsonb 列は、デフォルトで {"id": ..., "label": ..., "count": ..., "active": ...} という ~85 バイトの固定形オブジェクトを生成します。以前は完全ランダムな JSON を吐いていて 1 行が数 KB になることがあり、大量行で重かったため、最近この形に変えました。カラム固有の形が欲しければ value で上書きできます。

おわりに

seeder はまだ pre-1.0 で、現状は en / ja の 2 ロケール、JSON はプレースホルダ、という段階です。今後の方向性としては、

  • ロケールの追加
  • LLM 支援によるリアルなテキスト生成
  • 既存 DB の統計からのサンプリング (合成生成より実データ分布に近づける)

あたりを検討しています。「合成生成よりサンプリングのほうが嬉しい」みたいな意見があればぜひ聞きたいです。

フィードバック・Issue・PR お待ちしています。

https://github.com/mickamy/seeder

Discussion