第33章:Persistence Adapterの考え方(DBは“詳細”)🗄️✨
この章はひとことで言うと、**「DBの都合を、Core(Entities / UseCases)に一切入れない」**ための作法だよ〜!😺💕
1) 今日のゴール 🎯💡
章の終わりに、これができたら勝ち🏆✨
- ✅ UseCase側は 「IMemoRepository」みたいな “口(interface)” だけ知ってる
- ✅ DB(EF Core / SQL Server / SQLite…)は 外側の実装(Adapter) に押し込める
- ✅ InMemory → EF Core に差し替えても、UseCaseのコードがほぼ変わらない
- ✅ 「依存は内側へ」のルールを、コードとプロジェクト参照で守れてる (依存ルールの原典は Uncle Bob の Clean Architecture の説明がわかりやすいよ) (blog.cleancoder.com)
2) なんで「DBは詳細」なの?🤔🧩
DBは便利だけど、こんな“外側都合”が山ほどあるよね👇
- テーブル設計、カラム追加、インデックス…📚
- EF Coreの設定、マイグレーション、接続文字列…🔧
- DB製品の違い(SQL Server / PostgreSQL / SQLite…)🧠
これらは ビジネスルール(Core)の本質じゃない ので、外側に閉じ込めるのがクリーンアーキの気持ちよさ🌿✨ Microsoft のアーキテクチャガイドでも「UIはCoreのinterfaceを見て、Infrastructureの実装を知らないのが理想」って説明されてるよ (Microsoft Learn)
3) まず“絵”で理解しよ!🖼️➡️

ポイントはこれ👇
- Core:
IMemoRepositoryを 宣言(interfaceだけ) - Persistence Adapter:
EfMemoRepositoryを 実装(EF CoreのDbContextでゴリゴリ) - DI:実行時に “どっちの実装を刺すか” を決める(InMemoryでもEFでもOK)
イメージ(依存方向が超大事)👇
[Web(API)] ───────→ [Adapters(Persistence)] ───────→ [Core]
(参照する) (参照する) (参照しない)
ASP.NET Core EF Core / DB ルールと口(interface)だけ
4) 作るもの:Repoを InMemory → EF Core に差し替え 🪄💾
ここでは題材を「Memo」でいくね📝✨(他でも同じ!)
4-1. Core側:Repository “口” を定義する(interface)🔌
置き場所の例:
Core/UseCases/Ports/IMemoRepository.cs※ここに EF Coreの型(DbContext / DbSet / IQueryable)を絶対に入れない 🙅♀️
namespace MyApp.Core.UseCases.Ports;
public interface IMemoRepository
{
Task AddAsync(Memo memo, CancellationToken ct);
Task<Memo?> FindByIdAsync(Guid id, CancellationToken ct);
Task UpdateAsync(Memo memo, CancellationToken ct);
Task DeleteAsync(Guid id, CancellationToken ct);
}
🧠コツ:
- メソッドは UseCaseが本当に必要な分だけ に絞る(“汎用化しすぎ”禁止🚫)
IQueryableを返すのは、EFの都合が漏れやすいので初心者は特に避けよ〜🙅♂️💦
4-2. Adapter側:まず InMemory 実装で動かす 🧸✅
置き場所の例:
Adapters.Persistence/InMemory/InMemoryMemoRepository.cs
using System.Collections.Concurrent;
using MyApp.Core.UseCases.Ports;
namespace MyApp.Adapters.Persistence.InMemory;
public sealed class InMemoryMemoRepository : IMemoRepository
{
private readonly ConcurrentDictionary<Guid, Memo> _store = new();
public Task AddAsync(Memo memo, CancellationToken ct)
{
_store[memo.Id] = memo;
return Task.CompletedTask;
}
public Task<Memo?> FindByIdAsync(Guid id, CancellationToken ct)
{
_store.TryGetValue(id, out var memo);
return Task.FromResult(memo);
}
public Task UpdateAsync(Memo memo, CancellationToken ct)
{
_store[memo.Id] = memo;
return Task.CompletedTask;
}
public Task DeleteAsync(Guid id, CancellationToken ct)
{
_store.TryRemove(id, out _);
return Task.CompletedTask;
}
}
✅ここまでで UseCaseはDBを一切知らずに動く よ!うれしい〜!🥳🎉
5) いよいよEF Core実装(本命のPersistence Adapter)🧱🗄️
2026年1月時点では .NET 10 がLTSで、EF Coreも 10 系が提供されてるよ(例:10.0.2) (Microsoft for Developers)
5-1. Adapterプロジェクトに EF Core を追加 📦✨
(例:SQL Server の場合。SQLiteでもOKだよ〜!)
dotnet add .\MyApp.Adapters.Persistence\ package Microsoft.EntityFrameworkCore --version 10.0.2
dotnet add .\MyApp.Adapters.Persistence\ package Microsoft.EntityFrameworkCore.SqlServer --version 10.0.2
マイグレーションや dotnet ef を使うなら、ツールやDesignパッケージも必要になりやすいよ👇 (NuGet)
dotnet tool install --global dotnet-ef --version 10.0.2
dotnet add .\MyApp.Web\ package Microsoft.EntityFrameworkCore.Design --version 10.0.2
⚠️ multi-project構成だと「スタートアッププロジェクト(Web側)」に
Designが必要って怒られることがあるある! (Stack Overflow)
5-2. DB用モデル(永続化用)と DbContext を用意 🧱
本格的な「DomainモデルとDBモデルを分ける」は次章(第34章)で厚くやるけど、ここでは “雰囲気” を先に掴もう😺
using Microsoft.EntityFrameworkCore;
namespace MyApp.Adapters.Persistence.Ef;
public sealed class AppDbContext : DbContext
{
public DbSet<MemoRecord> Memos => Set<MemoRecord>();
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MemoRecord>(b =>
{
b.ToTable("Memos");
b.HasKey(x => x.Id);
b.Property(x => x.Title).HasMaxLength(200).IsRequired();
b.Property(x => x.Body).HasMaxLength(4000).IsRequired();
});
}
}
public sealed class MemoRecord
{
public Guid Id { get; set; }
public string Title { get; set; } = "";
public string Body { get; set; } = "";
}
5-3. EF Core Repository実装(=Persistence Adapter)🔁
using Microsoft.EntityFrameworkCore;
using MyApp.Core.UseCases.Ports;
namespace MyApp.Adapters.Persistence.Ef;
public sealed class EfMemoRepository : IMemoRepository
{
private readonly AppDbContext _db;
public EfMemoRepository(AppDbContext db) => _db = db;
public async Task AddAsync(Memo memo, CancellationToken ct)
{
_db.Memos.Add(ToRecord(memo));
await _db.SaveChangesAsync(ct);
}
public async Task<Memo?> FindByIdAsync(Guid id, CancellationToken ct)
{
var record = await _db.Memos.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == id, ct);
return record is null ? null : ToDomain(record);
}
public async Task UpdateAsync(Memo memo, CancellationToken ct)
{
_db.Memos.Update(ToRecord(memo));
await _db.SaveChangesAsync(ct);
}
public async Task DeleteAsync(Guid id, CancellationToken ct)
{
var record = await _db.Memos.FirstOrDefaultAsync(x => x.Id == id, ct);
if (record is null) return;
_db.Memos.Remove(record);
await _db.SaveChangesAsync(ct);
}
private static MemoRecord ToRecord(Memo memo) => new()
{
Id = memo.Id,
Title = memo.Title.Value,
Body = memo.Body.Value
};
private static Memo ToDomain(MemoRecord r)
=> Memo.Rehydrate(r.Id, new MemoTitle(r.Title), new MemoBody(r.Body));
}
🥰ここが最高ポイント:
- UseCaseは
IMemoRepositoryしか知らない - EFの都合(DbContext/DbSet/AsNoTracking/SaveChangesAsync)は 全部Adapterの中 → つまり「DBは詳細」を体現できてる✨ (Microsoft Learn)
6) DIで“差し替え”を完成させる 🧷✨
Web側(Composition Root)で登録するよ👇
using Microsoft.EntityFrameworkCore;
using MyApp.Adapters.Persistence.Ef;
using MyApp.Adapters.Persistence.InMemory;
using MyApp.Core.UseCases.Ports;
var builder = WebApplication.CreateBuilder(args);
// 例:開発はInMemory、本番はEF…みたいに差し替えできる🪄
if (builder.Environment.IsDevelopment())
{
builder.Services.AddSingleton<IMemoRepository, InMemoryMemoRepository>();
}
else
{
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IMemoRepository, EfMemoRepository>();
}
var app = builder.Build();
app.Run();
✅これで、UseCaseは1行も変えずに InMemory ⇄ SQL Server(EF Core) を切り替えられるよ〜!🎉🎉🎉
7) よくある事故パターン集 🚑💥(ここ超大事!)
❌事故1:CoreがEF Coreを参照しちゃう
DbContextをUseCaseに注入しちゃうIQueryableをCoreから返しちゃう → 依存ルール違反で、外側の都合が内側に侵入😇
❌事故2:Repositoryが“万能すぎる”
GetAll(),FindByCondition(...)みたいに増えがち → UseCaseごとの意図が消える⚠️
❌事故3:DTO/DBモデルをそのままCoreに持ち込む
- 永続化用の
MemoRecordをCoreで使いはじめる → 次章(第34章)の話に直結する罠🪤✨
8) ミニ課題(手を動かすやつ)✍️🔥
課題A:差し替え確認(超重要)✅
- InMemoryで動く
- EF Coreで動く
- UseCaseコードが 変わってない ことを確認🎯
課題B:DBプロバイダを変えてみる(おもしろい)🧪
- SQL Server → SQLite に変えても、UseCaseは無変更のまま (EF Coreは色んなDBをサポートしてるよ (NuGet))
9) Copilot / Codex に頼ると気持ちいいポイント 🤖💖
そのまま貼って使える“指示文”置いとくね👇
-
🧠設計レビュー 「
IMemoRepositoryのメソッドが汎用化しすぎてないか、UseCase目線で削れる案を出して。IQueryableは禁止で」 -
🔁マッピング安全性 「Domain↔DBモデル変換で落とし穴(null/既定値/ID/更新競合)を列挙して、ガード案も提案して」
-
🧰EF設定 「MemoRecordのテーブル設計(制約、インデックス候補)を、検索ユースケース前提で提案して」
10) まとめ 🌸✨
- Persistence Adapterは 「DBという詳細」を外側に閉じ込める壁 🧱
- Coreは interfaceだけ を持つ(実装を知らない)🔌
- DIで実行時に合体して、差し替え可能になる🪄
- 依存ルールが守れると、変更が怖くなくなる😌💕
次の第34章では、ここで出てきた 「DBモデルとDomainモデルを分ける(マッピングで吸収)」 を、もっと“実戦向け”に仕上げようね〜!🔁🔥