ORM
オブジェクト指向のコードとリレーショナル DB のテーブルをマッピングするライブラリ
ORM とは
ORM (Object-Relational Mapping) は、プログラミング言語のオブジェクトとリレーショナル DB のテーブルをマッピングし、SQL を直接書かずにデータベースを操作するライブラリである。
マッピングの実体は、クラス 1 つがテーブル 1 つ、クラスに並べた属性がその列、他のクラスへの参照が外部キーに対応するという取り決めだ。ORM はこの対応表を頼りに、メソッド呼び出しを SQL へ組み立て、返ってきた行をオブジェクトへ復元する。
対応が素直に付かない場所は必ず残る。オブジェクトの世界とテーブルの世界で表現力が食い違うためで、インピーダンスミスマッチと呼ばれる。継承や多態にはテーブル側に直接の対応物がなく、1 つの階層を複数テーブルに割るか 1 テーブルに詰めるかの選択が要る。多対多の関連は中間テーブルを介してしか表せない。同一性の扱いも異なり、同じ行を 2 回読んだときに同じオブジェクトを返すのか別のオブジェクトを返すのかは ORM の設計次第だ。ORM が短く書けるのはこの食い違いを内部で吸収しているからで、吸収の仕方が性能や整合性の問題として後から表に出てくる。
ORM の例
マッピングの決め方は製品ごとに違い、そこが使い勝手の差になる。
| ORM | 言語 | マッピングの決め方 |
|---|---|---|
| Prisma | TypeScript | 独自のスキーマファイルを正とし、そこから型とクライアントを生成する |
| Drizzle | TypeScript | SQL の構文に近い記述で、発行される SQL が読み手に見えやすい |
| TypeORM | TypeScript | クラスにデコレータを付けて定義する |
| SQLAlchemy | Python | ORM 層と、その下で SQL 式を直接組み立てる Core 層のどちらでも書ける |
| Django ORM | Python | フレームワーク付属で、モデル定義からマイグレーション生成まで一体 |
| Eloquent | PHP | 命名規約からテーブル名や主キーを推測する |
| GORM | Go | Go の構造体をそのままモデルとして扱う |
Prisma の使い方
Prisma はスキーマファイルを正とし、そこから型付きのクライアントを生成する。列名や関連の書き間違いは生成された型に存在しないため、実行前に落ちる。逆に、スキーマとデータベースの実体がずれれば型は嘘をつく。マイグレーションの適用漏れは型検査では捕まらない。
// schema.prisma
model User {
id String @id @default(uuid())
name String
email String @unique
orders Order[]
}
model Order {
id String @id @default(uuid())
amount Int
userId String
user User @relation(fields: [userId], references: [id])
}
// 型安全なクエリ
const user = await prisma.user.findUnique({
where: { id: '123' },
include: { orders: true }, // リレーションを含む
});
// user の型: User & { orders: Order[] }
include を書かない限り関連は返らない。公式ドキュメントは既定の戻り値を「モデルのスカラー列すべてと関連なし」と定めており、関連に触った瞬間に裏で問い合わせが飛ぶ遅延ロードを持たない。取得の範囲が呼び出し側の記述だけで決まるので、発行される SQL の本数が読みやすい。
ORM vs 生 SQL vs クエリビルダー
同じ処理を書くとき、どこまで SQL から離れるかで 3 段階に分かれる。離れるほど記述は短くなり、発行される SQL は見えにくくなる。
| アプローチ | 抽象度 | 型の効き方 | 例 |
|---|---|---|---|
| ORM | 高い | モデル定義や生成された型に無い列名はコンパイル時に落ちる | Prisma, TypeORM |
| クエリビルダー | 中程度 | メソッドを繋ぐ途中でも、列名と戻り値の型が追える | Knex |
| 生 SQL | 低い | 文字列として渡すため、誤りは実行時まで残る | pg ライブラリ |
境界は連続していて、Drizzle のように ORM と名乗りながらクエリビルダーに近い記述を採るものもある。この表は製品の分類ではなく、記述がどれだけ SQL から離れるかの目安として読むとよい。
ORM の落とし穴
N+1 問題
N+1 は ORM 固有の事故ではなく、関連を 1 件ずつ取りに行く書き方が原因になる。遅延ロードを持つ ORM (Django ORM、Eloquent、SQLAlchemy など) では関連属性を参照しただけで問い合わせが飛ぶため、ループの中で気づかずに踏む。関連を明示的に要求する Prisma では、次のように自分でループを書いたときに起きる。
// ❌ N+1: ユーザーごとに注文を個別取得
const users = await prisma.user.findMany();
for (const user of users) {
const orders = await prisma.order.findMany({ where: { userId: user.id } });
}
// ✅ include で一括取得
const users = await prisma.user.findMany({ include: { orders: true } });
include に切り替えると、問い合わせの本数がユーザー件数に比例しなくなる。Prisma は関連の読み込み方を問い合わせ単位で選ぶ機能をプレビューとして案内しており (2026 年 8 月時点・PostgreSQL / CockroachDB / MySQL 対応)、既定の join は PostgreSQL の LATERAL JOIN や MySQL の相関サブクエリで 1 本にまとめ、query はテーブルごとに問い合わせを分けてアプリ側で結合する。どちらを選んでも件数に依存しない本数に収まる点が N+1 との違いだ。検出の仕方や他の ORM での対策は N+1 問題 に整理してある。
複雑なクエリ
ORM では表現しにくい複雑なクエリ (ウィンドウ関数、CTE) は生 SQL にフォールバックする。
const result = await prisma.$queryRaw`
SELECT user_id, SUM(amount) as total
FROM orders
GROUP BY user_id
HAVING SUM(amount) > 10000
`;
生 SQL へ降りた瞬間、スキーマから型を生成する利点は消える。$queryRaw は型引数を渡さなければ戻り値が unknown で、渡した型が実際の列と合っているかは検証されない (公式ドキュメントも、生 SQL では実データが宣言した型と一致しない場合があると注記している)。集計関数の戻り値が bigint や numeric になるデータベースでは JavaScript 側で BigInt や Decimal に写るため、上の total をそのまま JSON へ渡すと詰まる。生 SQL の戻り値は境界で自分の型へ変換して受け取るのが安全だ。
DynamoDB と ORM
DynamoDB は RDB ではないため、従来の ORM は使えない。1 クラス 1 テーブルという前提が、複数種類の項目を 1 つのテーブルに同居させる設計と噛み合わないからだ。
DynamoDB 用のライブラリ (ElectroDB, DynamoDB Toolbox) が担うのは SQL を隠す仕事ではない。パーティションキーとソートキーの組み立て規則をコード側に宣言させ、同じテーブルに混在する項目を型で切り分ける方向の道具である。マッピングというより、キー設計の言語化に近い。
Lambda での選択
Lambda では接続の扱いが選択を左右する。同時実行数だけ実行環境が増え、環境ごとに接続を張るため、RDS では先に接続数の上限へ当たる。プールを実行環境の外へ出して接続をまとめる (RDS Proxy 等) か、接続を持たない経路を選ぶかの判断が ORM の選定より先に来る (コネクションプール)。生成物や依存が重い ORM はコールドスタートにも乗る。
| ケース | 推奨 |
|---|---|
| RDS + 複雑なリレーション | Prisma |
| RDS + パフォーマンス重視 | Drizzle or 生 SQL |
| DynamoDB | AWS SDK 直接 or ElectroDB |
選ぶときの基準
ORM を入れるかどうかは、隠れた分だけ見えなくなる SQL を許容できるかで決まる。読み書きの形が決まっていて件数も予測できる面なら、生成される SQL を毎回追わなくても運用は回る。逆に集計や分析寄りの問い合わせが主役で実行計画を握りたい面では、ORM の記述が薄い皮になり、結局生 SQL を読む時間が増える。
判断の順序としては、まず主要な問い合わせを SQL で書いてみて、それが ORM の語彙に素直に収まるかを見る。収まらない問い合わせが中心にあるなら、ORM は定型的な読み書きだけに使い、外れるものは最初から生 SQL に寄せた方が保守は楽だ。どの ORM を選ぶ場合も、発行された SQL をログに出す方法を最初に確認しておくこと。これを知らないまま本番に出すと、遅い原因の切り分けができなくなる。
この記事は役に立ちましたか?