/note/tech

ORMの技術選定で迷ったらPoEAAの設計パターンを思い出す

要約:

■ 1. ORM選定に設計パターンの理解が必要な理由

  • TypeScript ORMの選択肢の多さ:
    • Prisma、TypeORM、Drizzle、MikroORM、Sequelizeなどが存在する
    • どれを選んでも動くものは作れる
  • 雰囲気で選ぶことの問題:
    • 流行を理由に選ぶと、後で思っていたのと違うという事態になりうる
    • ORMごとに設計思想が異なり、向いているユースケースも違う
  • 本記事の狙い:
    • ORMを雰囲気ではなく理由で選ぶため、設計パターンを理解する
    • PoEAAの4パターンを振り返り、各ORMの設計意図を明らかにする
    • プロジェクトに合った技術選定を支援する

■ 2. PoEAAの概要

  • PoEAAとは:
    • Martin Fowlerが2002年に出版した書籍
    • Patterns of Enterprise Application Architectureの略
    • エンタープライズアプリケーション開発の設計パターンを体系化している
  • 現在への影響:
    • 20年以上経った今も多くのフレームワークやライブラリの設計に影響を与えている
  • 本記事で扱う範囲:
    • 収録パターンのうち、ORMに直接関係する4つに絞る

■ 3. Active Record

  • 定義:
    • ドメインオブジェクト自身がデータベースアクセスのロジックを持つパターン
    • Ruby on RailsのActiveRecordが代表例
  • TypeORMのActive Recordモードの例:
    • エンティティがBaseEntityを継承し、findOneやsaveのメソッドを持つ
    • User.findOneで取得し、プロパティを変更してuser.saveで保存する
  • メリット:
    • 直感的で学習コストが低い
    • コード量が少なくて済む
    • シンプルなCRUD操作に適している
  • デメリット:
    • ドメインロジックとDBアクセスが密結合になる
    • テストでDBのモックが必要になりやすい
    • 複雑なドメインロジックには向かない
  • 向いているケース:
    • 管理画面
    • シンプルなCRUD中心のアプリケーション
    • プロトタイプ開発

■ 4. Data Mapper

  • 定義:
    • ドメインオブジェクトとデータベースを完全に分離するパターン
    • 専用のマッパーまたはリポジトリが変換を担当する
  • MikroORMの例:
    • エンティティはchangeNameのようなドメインロジックを持つが、DB操作メソッドは持たない
    • 名前が2文字未満なら例外を投げるといった業務ルールをエンティティ内に記述する
    • EntityManagerやRepositoryが取得や永続化の操作を担当する
    • em.flushでUnit of Workにより変更をまとめてDBに反映する
  • メリット:
    • ドメインモデルがDBを意識しない
    • ドメインロジックを単体でテストでき、テストしやすい
    • 複雑なドメインロジックを表現しやすい
    • DDDとの相性が良い
  • デメリット:
    • コード量が増える
    • 学習コストが高い
    • シンプルなCRUDには過剰になりがち
  • 向いているケース:
    • 複雑なビジネスロジックを持つアプリケーション
    • ドメイン駆動設計を採用するプロジェクト

■ 5. Table Data Gateway

  • 定義:
    • テーブルごとにゲートウェイクラスを用意し、CRUD操作をまとめるパターン
    • 戻り値はドメインオブジェクトではなく、レコードセットやプレーンなオブジェクト
  • Prismaの例:
    • Prisma Clientがprisma.user.findUniqueのようなテーブル単位の操作を提供する
    • 取得結果はUserエンティティではなくプレーンなオブジェクト
    • 更新もprisma.user.updateでテーブル単位に行う
  • メリット:
    • シンプルで理解しやすい
    • SQLに近い感覚で操作できる
    • 型安全性を確保しやすい
  • デメリット:
    • ドメインロジックの置き場所を別途考える必要がある
      • サービス層に寄せる、ドメイン層を別途作るなどの対応が必要
    • 複雑なオブジェクトグラフの操作には向かない
  • 向いているケース:
    • CRUD中心だが型安全性が欲しいアプリケーション
    • サービス層でビジネスロジックを実装する設計

■ 6. Row Data Gateway

  • 定義:
    • テーブルの1行を表すオブジェクトがDB操作メソッドを持つパターン
    • 通常はドメインロジックを持たず、行のラッパーに徹する
  • 概念的な実装例:
    • UserRowクラスがupdateとdeleteのメソッドを持ち、内部でSQLを発行する
    • このパターンを純粋に実装したTypeScript ORMは少ない
  • Active Recordとの違い:
    • 違いはドメインロジックをどこに置くかにある
    • Active Recordはドメインと永続化が同居しやすい
    • Row Data Gatewayは永続化だけを担当し、業務ルールはドメイン層やサービス層に置く
  • 主要ORMで目立たない理由:
    • 現代のORMは体験の単純化か分離の徹底のどちらかに振り切る傾向が強い
    • そのためRow Data Gatewayは中間形態として扱われやすい

■ 7. TypeScript ORMの分類

  • Active Record系:
    • TypeORMのActive Recordモードは、Rails風でエンティティが操作メソッドを持つ
    • Sequelizeは10年以上の歴史があり、マイグレーション機能が充実している
  • Data Mapper系:
    • TypeORMのData Mapperモードは、リポジトリパターンでDDDと相性が良い
    • MikroORMはUnit of WorkとIdentity Mapを完備し、本格的なDDD向け
  • Table Gateway寄り:
    • Prismaはスキーマ駆動で型安全性が高い
    • Drizzleは軽量でSQLに近い記法を持ち、型安全
  • Query Builder:
    • KnexとKyselyはORMというよりクエリビルダ
    • テーブルや行を表すオブジェクトが永続化を担うというPoEAAの分類に綺麗には乗らない
    • データアクセス設計の自由度が高い代わりに、責務分割を自前で整える必要がある
  • 分類から見える設計意図:
    • TypeORMやSequelizeはRails的な開発体験の実現を目指している
    • MikroORMはHibernateのような本格的なData Mapperを目指している
    • PrismaやDrizzleは型安全性とシンプルさを重視し、独自のアプローチを取っている

■ 8. ORM選定の判断軸

  • ドメインロジックの複雑さ:
    • 複雑なビジネスロジックがありDDDを採用するなら、Data Mapper系が向いている
    • 具体的にはMikroORMやTypeORMのData Mapperモードが該当する
    • シンプルなCRUD中心ならActive Record系やTable Gateway系で十分
  • 型安全性の重要度:
    • TypeScriptを使う以上、型安全性は重視すべき
    • PrismaとDrizzleは型推論が強力で、スキーマから自動生成される型で安全なコードが書ける
  • SQLの制御粒度:
    • パフォーマンスチューニングで生SQLに近い制御が必要ならDrizzleやKyselyが適している
    • SQLを意識せずに開発したいならPrismaが適している
  • 既存の開発体験との親和性:
    • RailsのActiveRecordに慣れたチームなら、マイグレーションが充実したORMを選ぶとスムーズ
    • Prismaはスキーマファイルからマイグレーションを生成し、スキーマ駆動の体験に寄せられる
  • チームの学習コスト:
    • Data Mapper系は学習コストが高め
    • チームのスキルセットや新メンバーのオンボーディングを考慮して選ぶ必要がある

■ 9. ORMを使わない選択肢

  • 前提:
    • ソフトウェア開発で必ずORMを使わなければならない決まりはない
  • Query Builderのみを使う方法:
    • KnexやKyselyはPoEAAのパターンに直接対応しないが、十分に有力な候補
    • Kyselyではメソッドチェーンでselect、where、orderBy、limitを組み立てる
    • SQLの構造を保ちながら型安全性を得られる
    • オブジェクトへのマッピングやリレーション管理は自前で行う必要がある
    • その分クエリの挙動が予測しやすくなる
  • 生SQLと型定義の方法:
    • 最もシンプルなアプローチ
    • pgで手動定義したinterfaceを型引数に渡してクエリを実行する
    • sqlcやPgTypedを使えば、SQLファイルから型を自動生成できる
    • PgTypedではSELECT句から結果の型が推論される
  • ORMを使わなくてもよいケース:
    • 分析ダッシュボードやレポート生成など、複雑な集計クエリが多い読み取り中心の用途
    • パフォーマンスが極めて重要な場合
      • ORMが生成するSQLより生SQLのほうが高性能になることはよくある
    • スキーマが頻繁に変わらず、マイグレーションやスキーマ同期の恩恵が薄い場合
    • 生SQLに扱い慣れていて、ORMに縛られるのがつらい場合
  • ORMの恩恵を受けられるケース:
    • CRUD操作でリレーションを多用する場合
    • マイグレーション管理を統一したい場合
    • N+1の検出やEager LoadingをORMに任せたい場合

■ 10. ORMの有無に関わらず必要な能力

  • 業務経験:
    • ORMを利用するアプリケーションと利用しないアプリケーションの両方を業務で扱ってきた
  • 求められる能力:
    • いずれの場合もSQLを読み書きできる能力が求められる
    • ボトルネックを特定する能力が求められる
    • スロークエリに対して実行計画を読み、インデックスを適切に利用する能力が求められる
    • 非効率なSQLの記述を修正する能力が求められる
  • 結び:
    • 置かれている状況と照らし合わせて最適な選択をしてほしい

■ 11. 読者からの指摘

  • Active Recordのデメリット表現への疑問:
    • テストでDBのモックが必要になりやすいという表現には疑問がある
    • DBに依存する処理はテストDBで確認する方法もある
    • ドメインロジックをDBから切り離して単体テストしにくいという表現の方が正確