コンテンツにスキップ

シードデータ

空のスキーマは動きはしますが、何も見せてくれません。マイグレーション直後の データベースに対してアプリケーションを開けば、一覧はどれも空、詳細ページはどれも 404 です。そして誰もが最初にやるのは、手でデータを入れることです —— マシンごとに 違う内容で。

シードデータとは、その行を書き下したものです。データセットごとに1ファイル、 リポジトリの中に置き、コマンド1つで適用します。

用語 意味
シードデータ スキーマとは別に、データベースを使える状態にするために投入する行
データセット テーブル名を行のリストに対応させた YAML ファイル1つ
フィクスチャ テストが既知の状態として使うときの、同じファイルの呼び名 —— フィクスチャを参照

シードとマイグレーションの線引きは、正確に引いて おく価値があります。どちらもデータベースに何かを入れるからです。マイグレーションは 構造を変え、バージョンを持ち、適用済みとして記録され、本番を含むすべての環境へ 渡っていきます。シードはを挿入し、バージョンを持たず、どこにも記録されず、 開発とテストのデータベースに向けたものです。2回実行したときの意味も同じではありません。 マイグレーションは未適用のものを見つけませんが、シードはもう一度挿入します。

member:
- { id: 1, name: Frank }
- { id: 2, name: Grace }
- { id: 3, name: Heidi }

ファイルは testdata/seed/ に置きます。1つのファイルに複数のテーブルを書けます。 行は書いた順に挿入されるので、行どうしが参照し合う場合は、ファイルの中の順序も、 コマンドラインに並べたファイルの順序も効いてきます。

Terminal window
pw seed # ディレクトリ内のすべてのデータセット
pw seed users orders # この2つを、この順で

名前はシードディレクトリからの相対パスで、.yaml は省略できます。pw seed userspw seed users.yaml は同じ要求です。pw seed を参照して ください。

データセットは YAML だけです。モデルは DBUnit のもので、キーの名前もそこから来ていますが、 XML や CSV や Excel のデータセットは読みません。Popcorn Web にも、下層の dbtestify にも、それらを解析するものがない からです。

トップレベルのキーはテーブル名です。ただしアンダースコアで始まるものはディレクティブです。 各テーブルは行のリストを持ち、行は列名から値への対応です。

NULL は null と書きます。列を省いたときの意味は、周りの行がその列に触れているかどうかで 変わります。行はバッチで挿入され、1 つの文の列リストは、そのバッチの行が使ったキーの 和集合になるからです。

member:
- { id: 1, name: Frank, nickname: Frankie }
- { id: 2, name: Grace } # nickname は NULL として挿入される

そのテーブルのどの行も触れていない列は、挿入文そのものから外れます。だからスキーマの 既定値が効きます。既定値付きの created_at や連番の id をデータセットに書かずに 済むのはこれです。一方、どれか 1 行が値を与えた列は、隣の行にとっても同じ文の一部です。 省いた行に入るのは既定値ではなく NULL で、NOT NULL 制約があれば違反になり、無ければ 黙って空の列になります。

行には _tag のリストも書けます。Popcorn Web はこれを解析しますが、何にも使いません。 行のタグを参照して ください。

_operation: テーブルに最初に何をするか

Section titled “_operation: テーブルに最初に何をするか”

既定では各テーブルを truncate してから詰め直すので、同じデータセットを 2 回適用しても 行が倍になるのではなく同じ状態になります。

_operation:
member: insert
access_log: truncate
member:
- { id: 3, name: Heidi }
操作 効果
clear-insert(既定) テーブルを truncate してから、並んだ行を挿入する
insert すでにあるものを残したまま、並んだ行を挿入する
upsert 並んだ行を挿入し、主キーが既にあれば更新する
truncate テーブルを空にして、何も挿入しない
delete ファイルが主キーを挙げた行を削除する

ファイルが名前を挙げていないテーブルは、どの操作でも触られません。

主キーを必要とするのは upsertdelete の 2 つで、その参照にはコネクションが もう 1 本かかります。選ぶ前に読む価値のある制約です。

キーは単数形で、_tag も同じです。複数形はテーブル名として読まれるので、_operations: はディレクティブの綴り間違いではなく「そんなテーブルは無い」というエラーになります。

期待値としてしか意味を持たない値

Section titled “期待値としてしか意味を持たない値”

同じファイルは、読み込む代わりにデータベースと突き合わせることもできます。書式のうち 2 つは、その向きのためだけにあります。

角括弧で囲んだ値は、値ではなくマッチャです。

一致するもの
[null] NULL。null と書くのと同じ
[notnull] NULL 以外のすべて
[any] 任意の値
[currentdate, 2m] 現在時刻から指定した時間内のタイムスタンプ。既定は 1m
[regexp, ^User .+ logged in$] 文字列表現がパターンに一致する値
audit_log:
- { id: 1, created_at: [currentdate, 2m], message: [regexp, "^User .+ logged in$"] }

事前に書き下せない列——生成されるタイムスタンプ、識別子が埋め込まれたメッセージ——を これで覆えます。ただし、seed に使うファイルには書かないでください。マッチャはドライバに バインドできない値として届き、seed はそこで失敗します。

もう 1 つの期待値専用のキーが _match です。ファイルに並んでいない行を許すかどうかを テーブルごとに決めます。どちらを選ぶかは アサーション側と一緒に扱っています

テストヘルパーはまさにこのファイルを読みます。

server := testutil.TestRun(t, Handlers(), nil,
testutil.WithMigrations("../migrations"),
testutil.WithSeed("initial"),
)

WithSeed はスキーマの設置後、サーバの起動前にデータセットを読み込みます。 ディレクトリを変えるのが WithSeedDir です。ファイルを共有することは便利さの話では なく、これが目的です。シードとは別に管理されたフィクスチャはずれていきますし、その ずれ方は静かです。開発データベースにはもう存在しない形に対して、テストだけが通り 続けます。

ブラウザスイートは同じファイルの 3 人目の読み手です。pwdev ビルドのアプリケーション はシードエンドポイントを提供し、Playwright のテストはテストの合間に HTTP リクエスト 1 本でデータベースを入れ直します —— E2E テストが示すとおりです。形式は 1 つ、読み手は 3 人、ずれはありません。

テストはこのファイルを逆向きにも読みます。server.AssertDB(t, "after_archive") は データベースをデータセットと突き合わせ、テーブルごとの差分を報告します。リクエスト後の 期待状態が、SELECT のアサーションの列ではなく、このファイルの隣に置かれたもう 1 つの ファイルになるということです。その往復と、一致戦略、そしてテーブルを置き換えるのでは なく足すためのテーブルごとの操作については、 フィクスチャを参照してください。

シードの経路はマイグレーションと同じです。middleware.rdb.write_group、設定されて いれば middleware.rdb.migration_group、そして readonly の接続が選ばれることは ありません。

pw seed も DSN をアプリケーション自身の設定から解決するので、他と同じく APP_ENV に従います。その一貫性がこのコマンドを信頼できるものにしていますが、同時に危険にも しています。開発用でないデータベースへ流し込む前に、環境を確認してください。