メインコンテンツまでスキップ

第40章:DBドライバ/接続設定は外側に隔離🗄️➡️🌍

「SQLiteに繋ぐ処理」って、つい便利だから Repository の中に書きがちなんだけど… それやると、中心(UseCase/Entity)が“DBの都合”に引っ張られやすくなるんだよね🥲💦

この章では、DBドライバ生成・接続設定・初期化(PRAGMAやマイグレーション)を“外側の専用モジュール”に押し出すよ💪😺


まず結論🎯(ここだけ覚えればOK!)

  • ✅ Repository(Adapter)は 「DBに接続する」 をしない
  • ✅ Repository(Adapter)は 「もう接続済みのDBオブジェクト」を受け取って使うだけ
  • ✅ 接続文字列(ファイルパス/URL)、認証、PRAGMA、接続オプションは Frameworks & Drivers 側に置く
  • ✅ これで DB差し替えが超ラクになる🔁✨

“やりがちな事故”例💥(アンチパターン)

DB Driver isolation (Driver/Config separation)

Repositoryの中で、環境変数読んで、DB開いて、PRAGMA設定して…みたいなの👇

// ❌ これはやりがちだけど「外側の都合」が混ざりやすい例
import { DatabaseSync } from "node:sqlite";

export class SqliteTaskRepository {
private db = new DatabaseSync(process.env.DB_FILE ?? "app.db"); // ←設定が中に入ってる💦

constructor() {
this.db.exec("PRAGMA journal_mode = WAL;"); // ←初期化も中💦
}

// ... find/save など
}

こうなると…

  • DBファイル名変えたいだけで Repository 変更😵
  • テストで :memory: にしたいのに Repository が環境依存😵
  • SQLite以外へ移行したい時に “中心”へ波及しやすい😵

今どきのSQLite事情🆕✨(2026年1月時点)

  • Node.js には 標準の SQLite モジュール node:sqlite が用意されてるよ(ドキュメントもある)📚✨ (nodejs.org)
  • ただし node:sqlite は、環境や版によって experimental 扱いでフラグが必要なケースもある(例:--experimental-sqlite)⚠️ (GitHub)
  • そして “安定&高速な定番” としては better-sqlite3 が今も強い💪🔥(最新版も継続更新されてる) (GitHub)

この章のポイントは「どのドライバでも通用する隔離の型」なので、node:sqliteでもbetter-sqlite3でも同じ考え方でOKだよ😊🧡


正しい分離の形🏗️✨(登場人物を整理)

置き場所のイメージ📁

  • frameworks/db/DBを“作る”場所(接続・設定・初期化)
  • adapters/outbound/Repository実装(SQLを投げる・結果を変換する)
  • main(Composition Root):全部を組み立てる場所(注入💉)

ステップ1:外側に「DBを作るだけ」の工房を作る🛠️🗄️

ここは Frameworks & Drivers 側の責務ね😊

// frameworks/db/sqliteClient.ts
import { DatabaseSync } from "node:sqlite";
import path from "node:path";

export type SqliteDb = DatabaseSync;

export function createSqliteDb(options?: { file?: string }): SqliteDb {
const file = options?.file ?? path.join(process.cwd(), "data", "app.db");
const db = new DatabaseSync(file);

// 初期化(外側に寄せる✨)
db.exec("PRAGMA foreign_keys = ON;");
db.exec("PRAGMA journal_mode = WAL;");

return db;
}

node:sqlite の使い方自体は Node.js のAPI例にも載ってるよ📚 (nodejs.org) ※ もし環境によって experimental フラグが必要なら、起動コマンド側で対応する感じになるよ⚠️ (GitHub)


ステップ2:Repositoryは「接続済みDB」を受け取るだけにする🎁✨

// adapters/outbound/SqliteTaskRepository.ts
import type { SqliteDb } from "../../frameworks/db/sqliteClient";

export class SqliteTaskRepository {
constructor(private readonly db: SqliteDb) {}

findById(id: string) {
const stmt = this.db.prepare("SELECT id, title, completed FROM tasks WHERE id = ?");
const row = stmt.get(id) as { id: string; title: string; completed: 0 | 1 } | undefined;
if (!row) return null;

return {
id: row.id,
title: row.title,
completed: row.completed === 1,
};
}

save(task: { id: string; title: string; completed: boolean }) {
const stmt = this.db.prepare(
"INSERT INTO tasks(id, title, completed) VALUES(?, ?, ?) " +
"ON CONFLICT(id) DO UPDATE SET title=excluded.title, completed=excluded.completed"
);
stmt.run(task.id, task.title, task.completed ? 1 : 0);
}
}

ここで超大事なのは👇

  • Repositoryは DBファイル名も環境変数も知らない🙈
  • Repositoryは 接続やPRAGMAをしない🙅‍♀️
  • ただ SQLを実行して、結果を内側に渡しやすい形に変換するだけ🔄✨

ステップ3:Composition Root で組み立てる💉🧩

// main.ts(Composition Root)
import { createSqliteDb } from "./frameworks/db/sqliteClient";
import { SqliteTaskRepository } from "./adapters/outbound/SqliteTaskRepository";

const db = createSqliteDb({ file: "data/app.db" });
const taskRepo = new SqliteTaskRepository(db);

// ここから先で UseCase に taskRepo を注入していくイメージ✨

これで、DB設定を変えたい時は createSqliteDb だけ見ればOKになるよ😺👍


テストが気持ちよくなる例🧪✨(:memory: 最高!)

import { createSqliteDb } from "../src/frameworks/db/sqliteClient";
import { SqliteTaskRepository } from "../src/adapters/outbound/SqliteTaskRepository";

test("save -> findById", () => {
const db = createSqliteDb({ file: ":memory:" }); // ←差し替え一発😺
db.exec("CREATE TABLE tasks(id TEXT PRIMARY KEY, title TEXT, completed INTEGER);");

const repo = new SqliteTaskRepository(db);
repo.save({ id: "1", title: "milk", completed: false });

expect(repo.findById("1")?.title).toBe("milk");
});

この “差し替え一発感” が、クリーンアーキのご褒美だよ〜〜🎉💖


ありがちな落とし穴チェック✅😵‍💫

  • 😵 Repositoryのコンストラクタで new Database... してない?(→外側へ)
  • 😵 process.env を Repository が読んでない?(→外側へ)
  • 😵 PRAGMA/初期化/マイグレーションが Repository に混ざってない?(→外側へ)
  • 😵 “接続失敗”をドメインエラーっぽく扱ってない?(→技術エラーは外側寄りでOK)

AI相棒🤖✨(コピペで使えるプロンプト)

  • 🤖「Repository から process.env と new Database を消して、DB生成を別モジュールに分離して。差し替え可能な構成にして」
  • 🤖「PRAGMA とスキーマ初期化を frameworks/db 側へ寄せた設計に直して」
  • 🤖「テストで :memory: を使えるように依存注入に変えて。変更点の差分も説明して」

ミニ理解チェック🎓💡(1問だけ!)

Q. Repository が “DBファイルパス” を知っている設計は、何が困る?(2つ答えてね) 👀ヒント:差し替え・テスト・環境依存…


次の章(41章)で、この流れを Config(設定)の境界まで広げて「どこで読み、どこへ渡す?」をさらにキレイにするよ🧾✨