🪪

TypeScriptで、テーブルのIDがauto incrementな場合のモデルクラスの設計・実装

に公開

経緯

現在、新規サービスをドメイン駆動設計(DDD)で開発しています。その要件のひとつとして、ID を DB の AUTO_INCREMENT を用いた連番にする必要がありました。

以前のプロジェクトでは UUID v7 を ID に採用していたため、永続化前にアプリケーション側で ID を生成できました(UUID v7 は時系列順にソート可能な UUID です)。
今回は AUTO_INCREMENT を使う場合の設計と実装について整理します。

前提

サンプルとして、書籍を表すモデルクラスと、それを永続化するリポジトリを考えます。DB 側のテーブル定義は下表のとおりです。

論理名 物理名 制約
ID id INT AUTO_INCREMENT PRIMARY KEY
書籍名 title VARCHAR(255) NOT NULL

リポジトリに用意する永続化メソッドは save だけとし、呼び出し側が「永続化済みかどうか」を意識せずに済む設計とします。

実装案 1 — モデルの idundefined 許容にする

class Book {
  id?: BookId;
  title: string;
  constructor(id: BookId | undefined, title: string) { /* 代入は省略 */ }
}

interface BookRepository {
  findById(id: BookId): Book;
  save(model: Book): Book;
}

利点

  • 実装が非常にシンプル。

// 新規作成
const book = new Book(undefined, "不思議の国のアリス");
bookRepository.save(book);

// モデルの復元
const restored = new Book(new BookId(3), "不思議の国のアリス");

欠点

  • 参照側が常に「id が設定されているか」を意識しなければならず、TypeScriptの型安全性を十分に活かせない。

実装案 2 — リポジトリで “次の ID” を発行する

class Book {
  id: BookId;
  title: string;
  constructor(id: BookId, title: string) { /* 代入は省略 */ }
}

interface BookRepository {
  nextId(): BookId;
  findById(id: BookId): Book;
  save(model: Book): Book;
}

利点

  • Book インスタンスの生成時点で id が必ず確定するため、undefined 問題を解消できる。

// 新規作成
const id   = bookRepository.nextId();
const book = new Book(id, "不思議の国のアリス");
bookRepository.save(book);

欠点

  • 永続化を担うリポジトリが “ID 生成” まで巻き取るのは責務が曖昧になる。
  • 新規作成のたびに必ずリポジトリ経由でIDを取得する必要がある。

実装案 3 — ジェネリックで “未割り当て ID” を型レベルで表現

class Unassigned {} // 空クラスで「未割り当て」を表現

class Book<ID extends BookId | Unassigned = BookId> {
  id: ID;
  title: string;
  constructor(id: ID, title: string) { /* 代入は省略 */ }
}

interface BookRepository {
  findById(id: BookId): Book;
  save<ID extends BookId | Unassigned>(model: Book<ID>): Book;
}

利点

  • id の有無を型パラメータで明示できるため、復元済み/未復元をコンパイル時に判別できる。
  • 新規作成時にリポジトリへ ID を問い合わせる必要がない。

// 新規作成
const draft = new Book(new Unassigned(), "不思議の国のアリス"); // Book<Unassigned>
const saved = bookRepository.save(draft);                        // Book<BookId>

// モデルの復元
const restored = new Book(new BookId(3), "不思議の国のアリス"); // Book<BookId>

欠点

  • 型がやや複雑になり、保存前後でジェネリック型が変わる(Book<Unassigned>Book<BookId>)。

まとめ

3 案を比較すると、実装案3は

  • id の有無を型レベルで表現できる
  • 新規作成時にリポジトリへ都度問い合わせる必要がない

という利点があり、採用していきたいと思いました。

Discussion