第13章:Infrastructure入門① 永続化(DB/Storage)を外側に閉じ込める🗄️🚪✨
この章はね、「保存の都合(DBの型やSQLやファイル形式)」を中心(Domain/Application)に漏らさないための回です😊💡 ここを押さえると、あとでDBを変えても「え、全層修正…😇」みたいな地獄を回避できます✨
1) この章のゴール🎯💖
読み終わったら、こんなことができるようになります👇
- Domainは保存方法を知らない(DB/Storageの気配を消す🙈✨)
- Port(interface) を守りつつ、Infrastructureで Repository実装 が作れる🔌📦
- 保存形式 ↔ ドメイン の変換ポイント(マッピング)を迷わず置ける🧩
- インメモリ→SQLite→別DB みたいな 差し替え を想像できる🔁😊
2) まず結論:DB/Storageは「詳細」📌🧠

レイヤードの考え方だと…
- Domain:概念とルール(核💎)
- Application:手順と調整(ユースケース🎮)
- Infrastructure:DB/外部I/O/フレームワーク(外側の都合🚪)
だから保存はこう👇
✅ Application:「保存してね」と依頼する(interfaceを呼ぶ) ✅ Infrastructure:具体的に保存する(SQLite/Prisma/Drizzle/ファイル/Redis…) ❌ Domain:「PrismaClient import」とか絶対しない(匂いがしたらアウト😇💥)
3) 今日の題材:ToDoを例にするよ📝✨
Domain(例)
TodoId(VO)TodoTitle(VO:空禁止とか)TodoItem(Entity:idで追跡🪪)
Application
AddTodoUseCaseCompleteTodoUseCaseGetTodoListQuery(参照系)
Infrastructure(この章の主役👑)
TodoRepositoryの実装(DB/Storageの具体)
4) 「Port」は内側に置く🔌(復習ちょい😊)
第12章で作った想定の TodoRepository(Port)は、Application側に置くのが分かりやすいです✨
(Domainに置く流派もあるけど、今は迷子防止でApplicationに寄せよ〜🧭)
例:Application側(Port)
// src/application/ports/TodoRepository.ts
import { TodoId } from "../../domain/todo/TodoId";
import { TodoItem } from "../../domain/todo/TodoItem";
export interface TodoRepository {
save(todo: TodoItem): Promise<void>;
findById(id: TodoId): Promise<TodoItem | null>;
listAll(): Promise<TodoItem[]>;
}
ポイント👀💡
TodoRepositoryは Domainの型を返す(DBのRowを返さない🙅♀️)- メソッドは 小さめ(欲張ると巨大interfaceになって破綻しがち✂️)
5) いきなりDBに行かない!まずインメモリで勝つ🧸✨
Infrastructureは「差し替え」を見せると理解が一気に進むよ😊 最初に InMemory実装 を作って、ユースケースが動く状態にしよ〜🔁
// src/infrastructure/persistence/InMemoryTodoRepository.ts
import { TodoRepository } from "../../application/ports/TodoRepository";
import { TodoId } from "../../domain/todo/TodoId";
import { TodoItem } from "../../domain/todo/TodoItem";
export class InMemoryTodoRepository implements TodoRepository {
private store = new Map<string, TodoItem>();
async save(todo: TodoItem): Promise<void> {
this.store.set(todo.id.value, todo);
}
async findById(id: TodoId): Promise<TodoItem | null> {
return this.store.get(id.value) ?? null;
}
async listAll(): Promise<TodoItem[]> {
return [...this.store.values()];
}
}
これの良さ💖
- DBなしでユースケースがテストできる🧪✨
- 「Portを守ってる」感覚が身につく🔌😊
6) “保存形式”と“ドメイン”は別物だよ🧩📦
DBはだいたいこういう形になる👇
- DBの行:
{ id: string, title: string, is_done: 0/1, created_at: ... } - Domain:
TodoItem(VOや不変条件つきで安全💎)
だから マッピング(変換) が必要✨ この変換が “境界” のお仕事です🚪
✅ 方針:Infrastructureに「DB用モデル」を置く
// src/infrastructure/persistence/models/TodoRow.ts
export type TodoRow = {
id: string;
title: string;
completed: boolean;
createdAt: Date;
};
そして変換関数👇
// src/infrastructure/persistence/mappers/TodoMapper.ts
import { TodoItem } from "../../../domain/todo/TodoItem";
import { TodoId } from "../../../domain/todo/TodoId";
import { TodoTitle } from "../../../domain/todo/TodoTitle";
import { TodoRow } from "../models/TodoRow";
export const TodoMapper = {
toDomain(row: TodoRow): TodoItem {
return TodoItem.rehydrate({
id: TodoId.from(row.id),
title: TodoTitle.from(row.title),
completed: row.completed,
createdAt: row.createdAt,
});
},
toRow(todo: TodoItem): TodoRow {
return {
id: todo.id.value,
title: todo.title.value,
completed: todo.completed,
createdAt: todo.createdAt,
};
},
};
ここ重要〜〜‼️😳✨
toDomainは 不正データが来る可能性がある(DBの中身は100%信用できない🙈) →rehydrateみたいな「復元用」入口を用意すると整理しやすいよ🧠
7) SQLiteで永続化する実装例🗄️✨(Prisma版)
ローカル学習でいちばん楽なのは SQLite(ファイル1つでDBになる📄✨) そしてTypeScript界隈で強い選択肢が Prisma です。Prisma ORM 7.2.0 のリリースも出てます。(Prisma)
ちなみにTypeScript自体は npm 上だと 5.9.3 が “Latest” 表示です(直近の安定版目安)。(npm) Nodeは v24 が Active LTS、v25 が Current という整理になってます。(Node.js)
7-1) Prismaのセットアップ(例)
npm i prisma @prisma/client
npx prisma init --datasource-provider sqlite
prisma/schema.prisma(例)
model Todo {
id String @id
title String
completed Boolean @default(false)
createdAt DateTime @default(now())
}
マイグレーション&生成👇
npx prisma migrate dev --name init
npx prisma generate
7-2) PrismaRepository実装(Infrastructure)
// src/infrastructure/persistence/PrismaTodoRepository.ts
import { PrismaClient } from "@prisma/client";
import { TodoRepository } from "../../application/ports/TodoRepository";
import { TodoId } from "../../domain/todo/TodoId";
import { TodoItem } from "../../domain/todo/TodoItem";
import { TodoMapper } from "./mappers/TodoMapper";
export class PrismaTodoRepository implements TodoRepository {
constructor(private readonly prisma: PrismaClient) {}
async save(todo: TodoItem): Promise<void> {
const row = TodoMapper.toRow(todo);
await this.prisma.todo.upsert({
where: { id: row.id },
create: {
id: row.id,
title: row.title,
completed: row.completed,
createdAt: row.createdAt,
},
update: {
title: row.title,
completed: row.completed,
},
});
}
async findById(id: TodoId): Promise<TodoItem | null> {
const found = await this.prisma.todo.findUnique({
where: { id: id.value },
});
if (!found) return null;
return TodoMapper.toDomain({
id: found.id,
title: found.title,
completed: found.completed,
createdAt: found.createdAt,
});
}
async listAll(): Promise<TodoItem[]> {
const rows = await this.prisma.todo.findMany({
orderBy: { createdAt: "desc" },
});
return rows.map((r) =>
TodoMapper.toDomain({
id: r.id,
title: r.title,
completed: r.completed,
createdAt: r.createdAt,
})
);
}
}
7-3) 接続はどこで作る?→ Composition Root で💡🏗️
Repositoryの new は “入口でまとめて” がキレイ✨
// src/main.ts (例: Composition Root的な場所)
import { PrismaClient } from "@prisma/client";
import { PrismaTodoRepository } from "./infrastructure/persistence/PrismaTodoRepository";
import { AddTodoUseCase } from "./application/usecases/AddTodoUseCase";
const prisma = new PrismaClient();
const todoRepo = new PrismaTodoRepository(prisma);
const addTodo = new AddTodoUseCase(todoRepo);
// ここからPresentationに渡す…みたいな感じ😊
8) もう1つの選択肢:Drizzle(TSファースト)🧁✨
Drizzleも人気で、公式サイトで機能更新が継続してます。(Drizzle ORM) また、2026年1月時点で v1.0 のβ系バージョンが出ている動きも見えます。(Yarn)
なので選び分けイメージはこんな感じ👇
- Prisma:型安全+生成+体験が統一されてて学習がラクなこと多い🧠✨ (Prisma)
- Drizzle:TSでスキーマを書いて軽快、進化が速い🚀 (Drizzle ORM)
どっちでも大事なのは同じで👇 Domain/ApplicationにDBの匂いを入れないことです🙈💎
9) Infrastructureがやること/やらないこと✅🙅♀️
✅ やること(Infrastructureの責務)🛠️
- DB接続・クエリ実行
- 永続化モデル(Row)との変換
- マイグレーション運用(仕組み側)
- DB起因の例外を “アプリ向け” に整える(第18章で本格化⚠️)
❌ やらないこと(混ぜると地獄)😇
- Domainルールの判断(例:タイトル空OK?など)
- ユースケース手順(保存前に何するとか)
- UI用の表示整形(それPresentation🎨)
10) よくある事故あるある💥(回避テクつき🧯)
事故①:DomainがDBの型を持ち始める
Domain/TodoItemにcreated_atとかdbIdとか… → RowはInfrastructureだけに隔離🧱
事故②:Repositoryが肥大化して “なんでも屋” になる
searchByKeywordAndStatusAndDateRangeAnd...→ まずは ユースケースに必要な最小だけにする✂️✨
事故③:null/undefined地獄
DBは null を持てることが多い
→ toDomain で “復元時” にちゃんと弾く or 代替する🛡️
11) ミニ演習🧩✨(手を動かそ〜!)
演習A:差し替えをやってみる🔁
InMemoryTodoRepositoryを使ってユースケースが動くのを確認🧸PrismaTodoRepositoryに差し替え🗄️- コード側(Application/Domain)を一切変更せず動いたら勝ち🏆✨
演習B:マッピングを増やしてみる🧩
- Domainに
TodoPriority(VO)を追加(例:1〜3だけOK🔒) - DBには
priorityカラム追加 - 変換は
TodoMapperだけに閉じ込める🚪✨
12) AI活用🤖💡(この章と相性よすぎ!)
コピペして使ってOKな頼み方例だよ😊✨
- 「この
TodoMapper、責務が漏れてない?境界として適切?🧩」 - 「Repository interface が大きすぎないかレビューして✂️」
- 「DBのnullや型ズレが起きた時に安全に
toDomainする案を3つ出して🛡️」 - 「InMemory→SQLite に差し替えた時のテスト観点を列挙して🧪」
13) できたかチェック✅🌸
- DomainにDBライブラリのimportが1つも無い🙈✨
- Repositoryは
interfaceを守って差し替えできる🔌 - Row/Schemaの都合はInfrastructureに閉じている🧱
- 変換(Mapper)が “迷子” になってない🗺️
- InMemory→DB に変えても、ユースケース側は無改造で動く🔁🏆
次の章(第14章)では、外部APIを同じノリで「翻訳」して、外部のクセをDomainに絶対持ち込まないやり方をやるよ〜📡🈂️✨