■ 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から切り離して単体テストしにくいという表現の方が正確