【C#/Blazor】実務Webアプリ開発編 (33)DBのSeedデータを冪等に入れる ~アプリに必要なデータと開発用データを分ける~
実務Webアプリ開発編です。今回は、Seedデータ(初期データ)をどう入れるかを、EF Coreの例で整理します。
前回は、CQRSのクエリ側の書き方をみました。今回は、永続化とCQRSのパートの締めくくりとして、DBに最初に入れておくデータを扱います。
以下のような方に役立つ内容となっています。
この記事が役に立つ方
- 最初の管理者や開発用のサンプルデータを、どこでどう入れるか迷っている
- 起動のたびにSeedを実行して、データが増えたり例外になったりしないか不安
- 本番に入れるデータと、開発用のデータをどう分けるか整理したい
以下のようなMentorAppを題材として進めます。

GitHubにドキュメント・コードの一式があります。
prota-p/MentorApp: Blazor Server × クリーンアーキテクチャ × DDD の学習用リファレンス実装(実務Webアプリ開発編の題材)
初期データは、要件定義の回で要件の一つとして挙げていました。以下の記事とつなげて読むと理解しやすいです。
Seedは地味ですが、大事な仕組みです。本番は最初の管理者がいて、はじめて使い始められます。開発は試すデータがあれば、すぐ画面で確かめられます。
Seedデータは目的で2つに分ける
まず、Seedデータとは何か、何のために入れるのかを整理します。
Seedデータとは
MentorAppを開発環境で起動して、ユーザー一覧を開いてみます。誰も登録していないのに、ユーザーが7人並んでいます。

このユーザーたちは、アプリの起動時に入れたものです。このように最初に入れておくデータを、Seedデータと呼びます。「初期データ」と呼ぶこともあります。
よく見ると、管理者が2人います。「初期管理者」はアプリの設定で指定した1人で、本番でも作られます。READMEの手順で設定ファイルをコピーすると、仮の値が入ります。
「管理者(ダミー)」は、開発中に管理画面を試すためのユーザーで、本番にはいません。同じ管理者でも、入れる目的が違うのです。
アプリが動くのに必要なデータと、開発用データ
Seedデータは、入れる目的で次の表の2つに分けられます。
| アプリが動くのに必要なデータ | 開発用データ | |
|---|---|---|
| 例 | マスタ・既定の設定値・最初の管理者 | 画面を動かすためのダミーのユーザーや投稿 |
| 目的 | アプリを使い始められるようにする | 開発中に画面を動かして確かめる |
| 入る環境 | 本番を含め、アプリを動かす環境 | 開発環境など(本番には入れない) |
アプリが動くのに必要なデータは、マスタや既定の設定値、最初の管理者です。これがないと、本番でもアプリを使い始められません。先ほどの初期管理者は、こちらです。
開発用データは、開発中に画面を動かして確かめるためのものです。先ほどの管理者(ダミー)は、こちらです。
2つは、入る環境が違います。必要なデータは本番を含めて入れ、開発用データは本番には入れません。
開発用のデータまで、わざわざSeedで入れるものなの?
よくあるやり方です。開発中はDBをよく作り直しますが、起動するだけでデータがそろいます。全員が同じデータで試せるので、不具合も再現しやすくなります。
補足:テストのデータはSeedと分ける
「初期データ」と聞くと、テスト用のデータを思い浮かべる方もいるでしょう。テストのデータをSeedで用意する流儀もあります。
ただ、この連載ではSeedと別に扱います。各テストが自分でデータを用意し、Seedには頼りません。テストは後続のパートで扱います。
最初の管理者は誰が作るのか
必要なデータのうち、最初の管理者には「鶏と卵」の問題があります。
人を管理者にできるのが管理者だけなら、最初の1人を管理者にする人がいません。最初の管理者は、画面からは作れないのです。
そこで、最初の管理者は画面の外から作ります。誰を管理者にするかを設定で指定し、Seedで用意するのがその一つの方法です。
何度実行しても同じ状態にする(冪等)
Seedデータでもう一つ大事なのが、冪等(べきとう)にすることです。
冪等ってなに?
何度実行しても、同じ状態になることです。Seedなら、同じSeedデータで1回目でも10回目でも、DBの中身が同じになるように書きます。
Seedは何度も実行される
Seedは、起動のたびに実行されます。ネットショップの商品カテゴリを毎回追加すると、3件ずつ増えてしまいます。
// 起動のたびに3件ずつ増える
db.Categories.AddRange(
new Category("食品"),
new Category("日用品"),
new Category("家電"));
await db.SaveChangesAsync();カテゴリ名に一意制約があれば、増えはしません。そのかわり、2回目の起動で例外になり、アプリが止まってしまいます。
冪等にする3つの書き方
冪等にするには、入れる前に、もうあるかを確かめます。カテゴリを名前で探し、なければ追加します。これで、何度実行しても3件のままです。
foreach (var name in new[] { "食品", "日用品", "家電" })
{
// 名前で、もうあるかを確かめる
if (!await db.Categories.AnyAsync(c => c.Name == name))
db.Categories.Add(new Category(name));
}
await db.SaveChangesAsync();冪等にする書き方は、大きく分けて次の表の3つです。どれも冪等ですが、Seedデータを変えたときや、DBの行を消したときに違いが出ます。
| 書き方 | やり方 | 弱点 |
|---|---|---|
| 空なら全部追加 | テーブルが空かを確かめ、空ならまとめて追加する | 1件でもあれば何もしない。足したSeedデータも、消えた行も入らない |
| なければ追加 | キーで1件ずつ探し、なければ追加する(あれば何もしない) | 変えたSeedデータが、既存の行に反映されない。画面で消した行も、また入る |
| あれば更新、なければ追加(upsert) | キーで1件ずつ探し、なければ追加、あればSeedデータどおりに更新する | 画面で直した値も、実行するたびにSeedデータへ戻る。画面で消した行も、また入る |
「空なら全部追加」は、テーブルが空かだけを見ます。1件でもあれば何もしないので、最初に一度入れれば済むデータに向きます。
「なければ追加」は、1件ずつ探し、あれば何もしません。入れたあとは、人が画面で直すデータに向きます。
upsertは、あればSeedデータどおりに更新し、いつもSeedデータと同じにします。値をずっとSeedデータで決めるデータに向きます。
何で同じものを見分けるか
「なければ追加」にも、upsertにも、同じものを見分ける手がかりが要ります。手がかりは、次の表の3つから選びます。
| 手がかり | 例 | 使い方 |
|---|---|---|
| 代理キー(サロゲートキー) | 意味を持たないIDやGUID | Seedの側で値を固定するときだけ使える。DBが自動で振るなら、前もってわからない |
| 自然キー(ビジネスキー) | カテゴリの名前、ユーザーのメールアドレス | 業務の上で同じものを表す。一意制約を付けられる |
| 一意でない値の組 | トピックのメンタリングと題名 | 重複しうるが、Seedデータの中では見分けられる。一意制約は付けられない |
代理キーは、Seedの側でIDを固定したときだけ探すのに使えます。IDまで固定して入れる、変わらないマスタに向きます。
自然キーは、業務の上で同じものを表す値です。一意制約で守れるので、重複が許されないデータに向きます。
modelBuilder.Entity<Category>()
.HasIndex(c => c.Name)
.IsUnique();一意でない値の組は、業務では重複しうる値です。一意制約では守れないので、Seedデータの中で見分けられれば足りるデータに向きます。
EF CoreでのSeedの入れ方
ここまでの考え方は、どのフレームワークでも同じです。EF Coreには、Seedを入れる方法が主に次の表の3つあります。
| 方法 | いつ実行されるか | 向いているもの |
|---|---|---|
UseSeeding(EF Core 9から) | EnsureCreatedやマイグレーションの適用のたび | 汎用のSeed(公式の推奨) |
HasData | マイグレーションで反映(EnsureCreatedでは新しく作るときだけ) | マイグレーション以外では変わらないマスタ(主キーの指定が必須) |
| 自前の初期化処理 | アプリの起動時など、自分で呼んだとき | アプリの構造に合わせたい初期化 |
UseSeedingは、EnsureCreatedやマイグレーションの適用のたびに呼ばれます。公式の推奨で、汎用のSeedに向きます。
HasDataは、マイグレーションで反映します。主キーの指定が必須で、マイグレーション以外では変わらないマスタに向きます。
自前の処理は、自分で呼んだときに動きます。MentorAppは、環境ごとに入れるものを変え、Seedでリポジトリや設定も使うので、こちらにしています。
補足:起動時のSeedについて
公式は、Seedを通常の起動処理に含めないよう注意しています。MentorAppは学習用で、1台で動かす前提なので、起動時に入れています。
MentorAppの実装をみる
ここからはMentorAppの実装です。MentorAppはロールをenumで持ち、マスタのテーブルがないので、必要なデータは初期管理者だけです。
Seedに関わるファイルは、次の5つです。
src/
├── MentorApp.Infrastructure/
│ ├── Persistence/
│ │ ├── DatabaseInitializer.cs ← DB作成
│ │ ├── DevelopmentDataSeeder.cs ← 開発用データ
│ │ └── InitialAdminSeeder.cs ← 必要なデータ
│ └── InfrastructureInitialization.cs ← 窓口
└── MentorApp.Web/
└── Program.cs ← 呼び分けSeedのクラスは、Seedデータの2つの種類ごとに分けています。初期管理者はInitialAdminSeeder、開発用データはDevelopmentDataSeederです。
この章では、次の順に確かめていきます。
この章でみること
- 本番・開発・テストでSeedを呼び分ける
- 初期管理者:設定から作り、管理者がいなければ昇格する
- 開発用データ:なければ追加し、ドメインのルールを通す
本番・開発・テストでSeedを呼び分ける
起動時の処理は、Program.csの最後にあります。
// …(サービスの登録など・88 行省略)
// ========================================
// フェーズ3: 起動時処理
// ========================================
try
{
Log.Information("================================================================================");
Log.Information($"MentorApp 起動プロセス開始({app.Environment.EnvironmentName})");
Log.Information("================================================================================");
// 環境に応じたDB初期化
if (app.Environment.IsDevelopment() || app.Environment.IsProduction())
{
Log.Information("データベースを初期化しています");
await InfrastructureInitialization.InitializeRequiredDataAsync(app.Services);
if (app.Environment.IsDevelopment())
{
Log.Information("開発環境: サンプルデータを生成しています");
await InfrastructureInitialization.SeedDevelopmentDataAsync(app.Services);
}
}
else if (app.Environment.IsEnvironment("Testing"))
{
// Testing環境:DB初期化はスキップ
// テストコード(WebApplicationFactory)が明示的にDB作成・削除を管理する
Log.Information("Testing環境: DB初期化はテストフィクスチャーで管理されます");
}
else
{
// 想定外の環境名
Log.Warning("未定義の環境 '{EnvironmentName}' が指定されています。DB初期化をスキップします。", app.Environment.EnvironmentName);
}
app.Run();
// …(例外処理・5 行省略)
finally
{
Log.CloseAndFlush();
}環境ごとに、Infrastructure層の初期化の入口(窓口)であるInfrastructureInitializationの、どのメソッドを呼ぶかを分けています。図1が、その全体です。

本番(Production)では、InitializeRequiredDataAsyncだけを呼びます。DBを作り、アプリが動くのに必要なデータ(初期管理者)を用意します。
開発(Development)では、本番と同じ処理に続けて、SeedDevelopmentDataAsyncで開発用データも入れます。
テスト用のTesting環境では、起動時に何もしません。DBはテスト側が作り、終わったら消します。
初期管理者:設定から作り、管理者がいなければ昇格する
MentorAppの初期管理者は、設定のExternalIdで指定した、特定の1人です。ExternalIdは、ログインに使う外部の認証サービス(IdP)でのIDです。
初期管理者は、Adminロールを持つユーザー全般のことではありません。管理者(ダミー)も、後から画面でAdminにした人も、初期管理者ではありません。
初期管理者は、設定(InitialAdmin)のExternalIdがあるときだけ作ります。設定がなければ、何もしません。
/// <summary>
/// データベースの作成と初期管理者のシードを実行する
/// </summary>
/// <remarks>
/// Production/Development 環境の両方で実行される。
/// - データベースの作成(マイグレーション未使用時)
/// - 初期管理者のシード(設定されている場合)
/// Web層(Program.cs)から明示的に呼び出す。
/// </remarks>
public static async Task InitializeRequiredDataAsync(IServiceProvider services)
{
using var scope = services.CreateScope();
var scopedServices = scope.ServiceProvider;
// データベース作成
await DatabaseInitializer.EnsureCreatedAsync(scopedServices);
// 初期管理者のシード(UoW / Repository 経由)
var authOptions = scopedServices.GetRequiredService<IOptions<AuthenticationOptions>>().Value;
if (authOptions.InitialAdmin.IsConfigured)
{
var uowFactory = scopedServices.GetRequiredService<IUnitOfWorkFactory>();
var timeProvider = scopedServices.GetRequiredService<TimeProvider>();
var loggerFactory = scopedServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger(typeof(InfrastructureInitialization));
await InitialAdminSeeder.SeedAsync(
uowFactory,
timeProvider,
authOptions.InitialAdmin.ExternalId!,
authOptions.InitialAdmin.DisplayName,
authOptions.InitialAdmin.Email!,
logger);
}
}{
…(DBの設定・4 行省略)
"Authentication": {
"Provider": "Mock",
"InitialAdmin": {
"ExternalId": "YOUR_ENTRA_OBJECT_ID",
"DisplayName": "初期管理者",
"Email": "admin@example.com"
},
…(認証サービス・ログの設定・20 行省略)
}初期管理者を用意するのは、InitialAdminSeederのSeedAsyncです。
/// <summary>
/// 初期管理者をシードする
/// </summary>
/// <remarks>
/// appsettings.jsonの設定値に基づき、指定されたExternalIdを持つ
/// Admin権限のユーザーを作成する。本番環境でも使用する。
/// 対象ユーザーが既に存在する場合はロールを変更しない(意図して降格した管理者を、再起動で戻さないため)。
/// ただし管理者が1人もいない場合は、画面から回復できないため Admin に昇格する。
/// </remarks>
public static async Task SeedAsync(
IUnitOfWorkFactory uowFactory,
TimeProvider timeProvider,
string externalId,
string displayName,
string email,
ILogger logger)
{
if (string.IsNullOrWhiteSpace(externalId))
{
logger.LogWarning("初期管理者のExternalIdが設定されていません。スキップします。");
return;
}
if (string.IsNullOrWhiteSpace(email))
{
logger.LogWarning("初期管理者のEmailが設定されていません。スキップします。");
return;
}
await using var uow = await uowFactory.CreateAsync();
var existingUser = await uow.Users.FindByExternalIdAsync(externalId);
if (existingUser != null)
{
if (existingUser.Role == Role.Admin)
{
logger.LogInformation("初期管理者は既にAdminとして存在します: {DisplayName}", existingUser.DisplayName);
return;
}
if (await uow.Users.HasAnyByRoleAsync(Role.Admin))
{
logger.LogInformation("初期管理者のユーザーはAdminではありませんが、ほかにAdminがいるためロールは変更しません: {DisplayName} (Role: {Role})",
existingUser.DisplayName, existingUser.Role);
return;
}
var previousRole = existingUser.Role;
existingUser.ChangeRole(Role.Admin);
await uow.SaveChangesAsync();
logger.LogWarning("管理者が1人もいないため、初期管理者のユーザーをAdminに昇格しました: {DisplayName} (ExternalId: {ExternalId}, PreviousRole: {PreviousRole})",
existingUser.DisplayName, externalId, previousRole);
return;
}
var now = timeProvider.GetUtcNow();
var admin = new User(externalId, displayName, now, email, Role.Admin);
await uow.Users.AddAsync(admin);
await uow.SaveChangesAsync();
logger.LogInformation("初期管理者を作成しました: {DisplayName} (ExternalId: {ExternalId}, Email: {Email})",
displayName, externalId, email);
}初期管理者にはEmailも要ります。設定のEmailが空なら、警告をログに出して何もせずに戻ります。
判定の流れは図2のとおりです。まず、ExternalIdでユーザーを探し、初期管理者がもういるかを確かめます。

ExternalIdには一意制約もあり、同じユーザーが二重に登録されるのを、DBが最後に防ぎます。
private static void ConfigureUser(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>(entity =>
{
// Id はアプリ側で生成されるため、ValueGeneratedNeverを指定
entity.Property(x => x.Id).ValueGeneratedNever();
entity.Property(e => e.ExternalId)
.HasMaxLength(User.ExternalIdMaxLength);
// ExternalIdはIdPが発行するユーザー識別子。
// 同一ユーザーの二重登録を防ぐため、DB側でも一意性を保証する
entity.HasIndex(e => e.ExternalId)
.IsUnique();
entity.Property(e => e.DisplayName)
.HasMaxLength(User.DisplayNameMaxLength);
entity.Property(e => e.Email)
.HasMaxLength(Email.MaxLength)
.HasConversion(
email => email.Value,
value => new Email(value));
});
}見つからなければ、Adminとして作ります。2回目の起動からは見つかるので、作り直しません。
見つかって、すでにAdminなら何もしません。
Admin以外で見つかったときは、ほかにAdminがいればロールを変えません。Seedとしては、Adminが1人でもいれば足りるからです(開発環境では、管理者(ダミー)でもOKです)。
Adminが1人もいなければ、Adminに昇格します。Adminがいないと、画面からは誰もロールを変えられないからです。
設定さえあれば、どんなときでもAdminが1人はいるようにしているんだね。
冪等の章の書き方でいうと、基本は「なければ追加」で、Adminが0人のときだけロールを直す、という感じですね。
開発用データ:なければ追加し、ドメインのルールを通す
開発用データは、DevelopmentDataSeederが入れます。入れる順は、ユーザー・メンタリング・トピックです。
/// <summary>
/// 開発用データをシードする
/// </summary>
/// <remarks>
/// 6名のモックユーザー(admin×1, mentor×2, mentee×3)と関連データを作成する。
/// </remarks>
public static async Task SeedAsync(
IUnitOfWorkFactory uowFactory,
DateTimeOffset baseTime,
ILogger logger)
{
var timeSeq = new TimeSequence(baseTime);
var users = await SeedUsersAsync(uowFactory, timeSeq, logger);
var mentorships = await SeedMentorshipsAsync(uowFactory, users, timeSeq, logger);
await SeedTopicsAndMessagesAsync(uowFactory, mentorships, users, timeSeq, logger);
}メンタリングはユーザーを、トピックはメンタリングを参照するので、この順に入れます。
3つとも「なければ追加」です。それぞれ別に保存するので、途中で失敗すると一部だけ入りますが、次の起動で足りない分が入ります。
upsertにしないのは、画面で直した表示名やロールなどを、起動のたびに戻さないためです。
Seedデータは、3つともrecordの配列で書いています。ユーザーなら、この配列です。
// …(usingと定数・24 行省略)
private static readonly DevUserDefinition[] DevelopmentUsers =
[
new(DevUserIds.Admin1, "管理者(ダミー)", "admin1@example.com", Role.Admin),
new(DevUserIds.Mentor1, "メンター太郎(ダミー)", "mentor1@example.com", Role.Mentor),
new(DevUserIds.Mentor2, "メンター次郎(ダミー)", "mentor2@example.com", Role.Mentor),
new(DevUserIds.Mentee1, "メンティー花子(ダミー)", "mentee1@example.com", Role.Mentee),
new(DevUserIds.Mentee2, "メンティー桜(ダミー)", "mentee2@example.com", Role.Mentee),
new(DevUserIds.Mentee3, "メンティー梅(ダミー)", "mentee3@example.com", Role.Mentee)
];
// …(メンタリング・トピックのSeedデータと処理・252 行省略)冒頭のユーザー一覧のダミーの6人は、この配列から入ったものです。
開発環境のログイン画面には、IdPを通さずに入るボタンがあります。管理者・メンター・メンティーの3人分で、この配列と同じExternalIdを使います。

「なければ追加」で探すときの手がかりは、3つで違います。ユーザーは、自然キーのExternalIdで探します。
private static async Task<UserLookup> SeedUsersAsync(
IUnitOfWorkFactory uowFactory,
TimeSequence timeSeq,
ILogger logger)
{
await using var uow = await uowFactory.CreateAsync();
var addedCount = 0;
var userDict = new Dictionary<string, User>();
foreach (var def in DevelopmentUsers)
{
var existingUser = await uow.Users.FindByExternalIdAsync(def.ExternalId);
if (existingUser != null)
{
userDict[def.ExternalId] = existingUser;
logger.LogDebug("開発用ユーザーは既に存在します: {ExternalId}", def.ExternalId);
continue;
}
var user = new User(def.ExternalId, def.DisplayName, timeSeq.Next(), def.Email, def.Role);
await uow.Users.AddAsync(user);
userDict[def.ExternalId] = user;
addedCount++;
logger.LogDebug("開発用ユーザーを追加しました: {DisplayName} (ExternalId: {ExternalId})",
def.DisplayName, def.ExternalId);
}
if (addedCount > 0)
{
await uow.SaveChangesAsync();
logger.LogInformation("開発用ユーザーをシードしました: {Count}名のユーザーを追加", addedCount);
}
else
{
logger.LogInformation("開発用ユーザーは全て存在しています。追加なし。");
}
return new UserLookup(userDict);
}メンタリングは、メンターとメンティーの組で探します。
private static async Task<MentorshipLookup> SeedMentorshipsAsync(
IUnitOfWorkFactory uowFactory,
UserLookup users,
TimeSequence timeSeq,
ILogger logger)
{
await using var uow = await uowFactory.CreateAsync();
var mentorshipDict = new Dictionary<(string, string), Mentorship>();
var addedCount = 0;
foreach (var def in DevelopmentMentorships)
{
var mentorId = users.GetId(def.MentorExternalId);
var menteeId = users.GetId(def.MenteeExternalId);
var existingMentorship = await uow.Mentorships.FindByMentorAndMenteeAsync(mentorId, menteeId);
if (existingMentorship != null)
{
mentorshipDict[(def.MentorExternalId, def.MenteeExternalId)] = existingMentorship;
logger.LogDebug("開発用 Mentorship は既に存在します: {MentorId} - {MenteeId}",
def.MentorExternalId, def.MenteeExternalId);
continue;
}
var mentorship = new Mentorship(mentorId, menteeId, timeSeq.Next());
await uow.Mentorships.AddAsync(mentorship);
mentorshipDict[(def.MentorExternalId, def.MenteeExternalId)] = mentorship;
addedCount++;
logger.LogDebug("開発用 Mentorship を追加しました: {MentorId} - {MenteeId}",
def.MentorExternalId, def.MenteeExternalId);
}
if (addedCount > 0)
{
await uow.SaveChangesAsync();
logger.LogInformation("開発用 Mentorship をシードしました: {Count}件の関係を追加", addedCount);
}
else
{
logger.LogInformation("開発用 Mentorship は全て存在しています。追加なし。");
}
return new MentorshipLookup(mentorshipDict);
}トピックは、メンタリングと題名の組で探します。同じメンタリングで題名が重複しうる(一意制約もない)ので、冪等の章でみた「一意でない値の組」です。
private static async Task SeedTopicsAndMessagesAsync(
IUnitOfWorkFactory uowFactory,
MentorshipLookup mentorships,
UserLookup users,
TimeSequence timeSeq,
ILogger logger)
{
await using var uow = await uowFactory.CreateAsync();
var addedCount = 0;
foreach (var def in DevelopmentTopics)
{
var mentorship = mentorships.Get(def.MentorExternalId, def.MenteeExternalId);
var existingTopic = await uow.Topics.FindByMentorshipAndTitleAsync(mentorship.Id, def.Title);
if (existingTopic != null)
{
logger.LogDebug("開発用 Topic は既に存在します: {Title}", def.Title);
continue;
}
var topic = new Topic(mentorship.Id, def.Title, timeSeq.Next(TopicTimeIncrement));
foreach (var msgDef in def.Messages)
{
topic.PostMessage(users.GetId(msgDef.SenderExternalId), msgDef.Content, timeSeq.Next());
}
await uow.Topics.AddAsync(topic);
addedCount++;
logger.LogDebug("開発用 Topic を追加しました: {Title} ({MessageCount}件のメッセージ)",
def.Title, topic.Messages.Count);
}
if (addedCount > 0)
{
await uow.SaveChangesAsync();
logger.LogInformation("開発用 Topic と Message をシードしました: {Count}件のトピックを追加", addedCount);
}
else
{
logger.LogInformation("開発用 Topic は全て存在しています。追加なし。");
}
}3つとも、エンティティのコンストラクタとメソッドを通して作ります。トピックなら、new Topicで作り、PostMessageでメッセージを足します。
そのため、題名やメッセージの長さといった、エンティティのルールは、Seedでも守られます。
TopicとMessageのルールは、第21回で扱っています。
日時は、今の時刻を使わず、固定した基準の時刻から少しずつ進めます。冒頭のユーザー一覧でも、ダミーの6人の登録日時は1分刻みです。
空のDBから作れば、開発用データの名前や日時は、毎回同じになります。
Seedを2回目に実行したときのログです。初期管理者も開発用データも、すでにあるので何も足していません。
[17:35:28 INF] 初期管理者は既にAdminとして存在します: 初期管理者
[17:35:28 INF] 開発環境: サンプルデータを生成しています
[17:35:28 INF] 開発用ユーザーは全て存在しています。追加なし。
[17:35:28 INF] 開発用 Mentorship は全て存在しています。追加なし。
[17:35:28 INF] 開発用 Topic は全て存在しています。追加なし。何度Seedを実行しても、データが増えたり変わったりしないんだね。
まとめ
今回は、Seedデータの分け方と、何度実行しても同じ状態にする書き方を整理しました。重要なポイントは以下です。
今回のポイント
- Seedデータは、アプリが動くのに必要なデータと開発用データに分け、環境ごとに入れるものを変える
- Seedは起動のたびに実行されるので、何度実行しても同じ状態になるように(冪等に)書く
- 書き方は、入れたあとの値を誰が決めるかで選ぶ。画面で直すなら「なければ追加」、Seedデータで決め続けるならupsert
- 既存を探す手がかり(代理キー・自然キー・一意でない値の組)を、データに合わせて選ぶ
次回からは認証のパートに入り、OIDCによる認証を扱います。今回、初期管理者の指定に使ったExternalIdが、どこから来るのかもみていきます。
Seedは目的で2つに分け、どちらも何度実行しても同じ状態になるように書く。これで安心して起動できますね。
引き続き、実務的なWebアプリの開発について一緒に学んでいきましょう!




