別のPCでクローンしたら No such table: Dishes──ローカルSQLiteの“ファイル=データベース”という仕様を忘れると、開発体験は一気に険しくなります。本稿は .NET 9 の Minimal API と Entity Framework Core を前提に、ノートPCとデスクトップ間でSQLiteを確実に共有・再現するための実践解を、エラー原因の切り分けから再発防止まで網羅的に解説します。
症状の整理:No such table: Dishes が出る理由
エラーの本質は「アプリが参照している .db ファイルに Dishes テーブルが存在しない」ことです。SQLiteは“1ファイル=1データベース”のため、そのファイル自体が無ければ当然テーブルもありません。Gitでソースだけを共有し、DBファイルを共有していない場合に高確率で発生します。
- ノートPCで生成された
Data/Dishes.dbが、デスクトップ側のクローンに存在しない。 - 存在していても、異なるパスを見に行っている(相対パス/実行フォルダー差異)。
- EF Coreのマイグレーションが未適用(スキーマが作られていない)。
なお、.vs/slnx.sqlite はVisual Studioのソリューション設定用データベースであり、アプリの実データベースではありません。ここにテーブルを作ろうとしても意味はありません。
解決の全体像:4つの方策とトレードオフ
まずは対応方針を俯瞰しましょう。基本的には「ソースからDBを再構築する(A/C)」か「DBファイルごと共有する(B)」、あるいは「そもそもサーバーDBに移行する(D)」のいずれかです。
| 方策 | 手順・ポイント | 長所 | 注意点 |
|---|---|---|---|
| A. 各マシンでDBを自動生成(推奨) | 1. Migrations フォルダーをリポジトリに含める2. クローン後に dotnet ef database update を実行 または 起動時に Database.Migrate() を呼び出す | Gitにバイナリを置かず軽量/常に最新スキーマを再現 | 初期データが必要ならSeed処理をコードに追加 |
B. .db ファイルをリポジトリに含める | 1. .gitignore から当該 .db を除外2. git add Data/Dishes.db → git commit/push | クローンだけで即動作 | 差分管理に不向き/情報漏えい・肥大化リスク/頻繁更新はGit LFS推奨 |
| C. 起動時にSeedデータを挿入 | Database.Migrate() 後に if (!db.Dishes.Any()) { ... } でテストデータ投入 | どの環境でも同じ初期データ | 本番と混同しない環境判定が必須 |
| D. 共有アクセス要件ならサーバーDBへ | SQLiteは同時書き込みに弱い。Web API + サーバーRDBMS(Azure SQL / PostgreSQL 等)へ移行 | 同時編集・スケール容易 | 環境構築コスト増 |
まずはここから:接続文字列と基準パスの落とし穴
「DBが無い」だけでなく「パスを見失っている」ケースもよくあります。相対パスは実行コンテキストに依存するため、Visual Studio実行と dotnet run、テスト実行で基準フォルダーが変わり得ます。
// appsettings.json(一例)
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=Data/Dishes.db"
}
}
この指定は 実行フォルダー(例:bin/Debug/net9.0)を基準に解決される場合があり、環境により Data/Dishes.db が見つからないことがあります。もっと安全にするには ContentRoot へ明示的に解決しましょう。
// Program.cs(.NET 9 Minimal API)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
{
var contentRoot = builder.Environment.ContentRootPath;
var dbPath = Path.Combine(contentRoot, "Data", "Dishes.db");
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
options.UseSqlite($"Data Source={dbPath}");
});
var app = builder.Build();
// 起動時にマイグレーション&Seed
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate();
SeedData.Run(db, app.Logger, app.Environment);
}
app.MapGet("/dishes", async (AppDbContext db) => await db.Dishes.OrderBy(x => x.Id).ToListAsync());
app.Run();
| 実行方法 | 基準パス | 典型的なDB実体の場所 | 対策 |
|---|---|---|---|
| Visual Studio デバッグ実行 | ContentRootPath(プロジェクトルート) | <プロジェクト>/Data/Dishes.db | ContentRootPathからの絶対化を徹底 |
dotnet run | カレントディレクトリ | 同上(ただし cd 先に依存) | ContentRoot明示/絶対パス生成 |
| xUnit などテスト | テストホストの作法次第 | 見失いがち | テスト用にインメモリや一時ファイルを利用 |
方策A:マイグレーションで各マシンにDBを自動生成(推奨)
ソースからスキーマを再現する王道パターンです。チーム開発にも最適。
必要パッケージ
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-ef
# or ローカルツール
dotnet new tool-manifest
dotnet tool install dotnet-ef
dotnet tool restore
モデルとDbContextの最小実装
public class Dish
{
public int Id { get; set; }
public string Name { get; set; } = default!;
public int Price { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options) : base(options) {}
public DbSet Dishes => Set();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Dish>()
.Property(x => x.Name)
.HasMaxLength(120)
.IsRequired();
modelBuilder.Entity<Dish>()
.HasIndex(x => x.Name);
}
}
マイグレーション作成と適用
# 初回
dotnet ef migrations add InitialCreate
# スキーマ変更時
dotnet ef migrations add AddDishPriceIndex
# 適用(ローカルDBを作成/更新)
dotnet ef database update
Migrations フォルダーは必ずコミットします。これにより、クローン直後のPCでも dotnet ef database update だけで同じスキーマを再現可能です。
アプリ起動時に自動適用する(安全網)
前述の db.Database.Migrate() を起動時に呼び出しておけば、開発中はコマンドを忘れても自動で最新化できます。CIや本番での適用は、ロールバック戦略や権限設計の都合で別運用にすることもあります。
Seedデータ(テスト・デモ用)
public static class SeedData
{
public static void Run(AppDbContext db, ILogger logger, IWebHostEnvironment env)
{
if (!env.IsDevelopment()) return; // 本番混入を防ぐ
if (!db.Dishes.Any())
{
db.Dishes.AddRange(
new Dish { Name = "チキンカレー", Price = 880 },
new Dish { Name = "ペペロンチーノ", Price = 900 },
new Dish { Name = "味噌ラーメン", Price = 850 }
);
db.SaveChanges();
logger.LogInformation("Seeded initial dishes.");
}
}
}
EnsureCreated() と Migrate() の違い
EnsureCreated()はマイグレーション履歴を使わず、空のDBに対して一気にスキーマを作ります。既存DBの進化には不向き。Migrate()はマイグレーション履歴を用いて段階的に更新します。チーム開発や長期運用ではこちらが必須。
方策B:.db ファイルをリポジトリに含める(手軽だが慎重に)
デモや小規模ツールなら「クローンしたらすぐ動く」利点は大きいですが、以下を必ず検討してください。
- 機微情報(個人情報・秘密データ)を含めない。
- ファイル肥大化・コンフリクト対策にGit LFSを使う(頻繁更新時)。
- SQLiteのWAL/journalファイル(
.db-wal,.db-shm等)を正しく扱う。基本はクローズ後に.db本体だけをコミット。
# 例:LFSで追跡
git lfs install
git lfs track "Data/*.db"
git add .gitattributes Data/Dishes.db
git commit -m "Add seeded SQLite DB"
git push
将来スキーマを変更するなら、結局はマイグレーションを運用することになります。学習コストを避けず、方策Aへ寄せるのが長期的には安全です。
方策C:起動時にSeedデータを挿入する(再現性の担保)
テーブルは空でも動くが、画面やAPIの動作確認に最低限のサンプルが欲しい──そんな時にSeedが効きます。環境判定(Developmentのみ等)を必ず入れ、本番DBにテストデータが混入しないよう徹底しましょう。
| Seedの目的 | 実装ポイント | アンチパターン |
|---|---|---|
| UI/APIの即時確認 | 起動時に空チェック→追加 | 毎回Insertして重複作成 |
| ドメインのサンプル共有 | 環境変数やビルド構成でON/OFF | 本番でもSeedを常時実行 |
| テスト再現性 | テストプロジェクト側で独自に準備 | 本番DBをテストに使い回す |
方策D:同時編集したいならサーバーRDBMSへ移行
SQLiteは軽量・簡単が魅力ですが、複数クライアントからの同時書き込みや遠隔共有に向きません。Web API配下で複数PCからアクセスするなら、SQL Server / PostgreSQL / MySQL などのサーバーRDBMSを選び、接続文字列はユーザーシークレットや環境変数で管理しましょう。
// 例:PostgreSQLへ切り替え
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
# ローカル秘密情報に格納(ユーザーシークレット)
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Host=localhost;Database=appdb;Username=app;Password=secret"
クラウドストレージ同期は最終手段
OneDrive/Dropbox/Google Drive等で .db を共有すると、ロック競合やWALファイルとの不整合で破損リスクが上がります。読み取り専用・単独編集のドキュメント閲覧用途ならまだしも、開発中のDBの同時書き込み共有には使わないでください。
Visual Studioの .vs/slnx.sqlite について
繰り返しになりますが、.vs/slnx.sqlite はソリューションのIDE内部データ用のSQLiteです。ここにアプリのテーブルを作ることはできても意味がありません。Git管理の対象外にして構いません。
実践レシピ:クローン直後に確実に動かす手順(チェックリスト)
- リポジトリをクローン(
git clone ...)。 - ソリューション直下に
Dataフォルダーが存在するか確認。無ければ作成。 - 方策Aの場合:
dotnet tool restore→dotnet ef database update。
または起動時Migrate()が走る実装にしておく。 - 方策Bの場合:
Data/Dishes.dbが存在し、アプリの接続先と一致しているか確認。 - アプリを起動。
/dishesなどのエンドポイントで取得できるか確認。 - まだエラーなら次の「トラブルシュート」を実施。
トラブルシュート:それでも No such table が消えないとき
- 接続先が違う:ログに出るDBパス(
Data Source=...)を必ず記録。想定と一致しなければ、ContentRootを基準に絶対パスを組み立てる。 - マイグレーション履歴が空:
dotnet ef migrations listで確認。空ならモデルから作成(dotnet ef migrations add InitialCreate)。 - 実DBにテーブルが無い:SQLiteツールで
SELECT name FROM sqlite_master WHERE type='table';。Dishesが無ければdatabase updateを適用。 - 複数DbContext:接続文字列やマイグレーション履歴のスコープがコンテキストごとにズレていないか確認。
- 構成ファイルの切り替え:
appsettings.Development.jsonとappsettings.jsonのどちらが読まれているか。環境変数ASPNETCORE_ENVIRONMENTをチェック。
セキュリティと情報管理:DBをGitに入れる前に
SQLiteファイルには全データが平文で入っています。公開リポジトリにコミットするのは怪我のもと。社内でもアクセス制御・暗号化・秘匿情報分離を検討しましょう。機微データを含むなら、そもそも方策Bは選ばず、方策A+Seedや方策Dへ。
開発効率を上げる周辺Tips
CIでマイグレーションを検証
# GitHub Actions 例(抜粋)
dotnet restore
dotnet build --no-restore
dotnet tool restore
dotnet ef database update --project src/App --startup-project src/App
dotnet test
インメモリ/一時ファイルでのテスト
// インメモリSQLite(注意:複数接続やライフタイムに癖あり)
var keepAlive = new SqliteConnection("DataSource=:memory:");
keepAlive.Open();
options.UseSqlite(keepAlive);
絶対パス生成のユーティリティ化
public static class DbPath
{
public static string FromContentRoot(IHostEnvironment env, params string[] segments)
{
var path = segments.Prepend(env.ContentRootPath).ToArray();
var full = Path.Combine(path);
Directory.CreateDirectory(Path.GetDirectoryName(full)!);
return full;
}
}
よくある質問(FAQ)
Q:.dbをコピーしたのにエラーは消えない?
A:アプリが別の場所を見ています。起動ログの接続文字列で実際のパスを確認し、ContentRoot 経由の絶対パスに統一しましょう。
Q:EnsureCreated() ではダメ?
A:短命なPoCならアリですが、スキーマが進化すると破綻します。チーム/長期運用は Migrate() 一択です。
Q:OneDriveで同期して共同編集できる?
A:書き込み競合で破損のリスクが高いです。読み取り専用での共有に限定するか、方策Dを検討してください。
Q:.db-wal や .db-shm ファイルはコミットすべき?
A:基本は不要です。アプリを完全に終了させ、.db 本体のみコミットしてください。
Q:マイグレーションを本番で自動適用してよい?
A:開発はOKでも本番は要審査です。停止時間・バックアップ・ロールバックの設計とセットで運用してください。
サンプル一式:最小構成で“再現できる”プロジェクト
以下のファイル群を持つだけで、どのPCでも同じスキーマ+サンプルデータを自動生成できます。
Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext(opt =>
{
var dbPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "Dishes.db");
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
opt.UseSqlite($"Data Source={dbPath}");
});
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService();
db.Database.Migrate();
SeedData.Run(db, app.Logger, app.Environment);
}
app.MapGet("/dishes", async (AppDbContext db) => await db.Dishes.ToListAsync());
app.MapPost("/dishes", async (AppDbContext db, Dish input) => {
db.Add(input);
await db.SaveChangesAsync();
return Results.Created($"/dishes/{input.Id}", input);
});
app.Run();
AppDbContext.cs / Dish.cs
public class Dish
{
public int Id { get; set; }
public string Name { get; set; } = default!;
public int Price { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options) : base(options) {}
public DbSet Dishes => Set();
}
SeedData.cs
public static class SeedData
{
public static void Run(AppDbContext db, ILogger logger, IWebHostEnvironment env)
{
if (!env.IsDevelopment()) return;
if (db.Dishes.Any()) return;
db.Dishes.AddRange(
new Dish { Name = "ハンバーグ", Price = 980 },
new Dish { Name = "カルボナーラ", Price = 1100 },
new Dish { Name = "牛丼", Price = 650 }
);
db.SaveChanges();
logger.LogInformation("Seed completed.");
}
}
マイグレーションの作成と共有
dotnet ef migrations add InitialCreate
git add Migrations
git commit -m "Add InitialCreate migration"
git push
発展:運用で困らないための設計指針
- 同じコードで同じ結果:マイグレーションとSeedで「再現可能性」を保証。
- パスは絶対化:
ContentRoot基準に統一。相対パスをやめる。 - 秘密はコードに置かない:接続文字列はユーザーシークレット/環境変数。
- DBをコミットするなら理由を書け:READMEに意図・運用・注意点を明記。
- 将来を見据える:同時編集・スケールが必要なら早めにサーバーRDBMSへ。
まとめ
No such table: Dishes の本当の原因は「ファイルがない/スキーマがない/パスが違う」のいずれかです。SQLiteは“ファイル=データベース”であることを踏まえ、方策A:マイグレーションで自動再生成+Seed を基本戦略に据えれば、ノートPC⇔デスクトップ間の移動でもエラーなく同じ状態を即時再現できます。データ共有・同時編集まで踏み込むなら、早期にサーバーRDBMSへ移行する判断が、将来の保守コストを大きく下げます。

コメント