- ORMは「スタートアップ向けのおもちゃ」と批判されることがあるが、実際の論点はORMそのものよりも誤った使い方に近い
- オブジェクトグラフとリレーショナルテーブルは異なるモデルであるためパラダイムの不一致が生じるが、それだけでORMをアンチパターンと見るのは難しい
- SRPとSOC違反をめぐる議論は妥当だが、ORMはそもそも2つのデータモデルをつなぐ抽象化であり、原則違反の有無だけで判断するのは難しい
- パフォーマンス問題はORMよりも、ホスト言語のロジックでデータを組み合わせるパターンでよく発生し、ORMのSQL風の機能で軽減できる
- LagoがActive Recordクエリを生SQLに置き換えた主な理由は速度よりも可視性とデバッグであり、大きなクエリでは生SQLへの移行が実務上有利な場合がある
ORM批判を見る基本的な立場
- ORMはしばしば「アンチパターン」として攻撃されるが、ORMそのものが悪いという結論は誇張に近い
- 他の抽象化と同様に、ORMも完璧ではなく、一部の可視性の低下やパフォーマンスコストを生むことがある
- LagoはRuby on RailsのORMであるActive Recordに依存している中で問題に直面し、それをきっかけにORMへの依存の仕方を再検討した
- 結論はORMを捨てるべきだという方向ではなく、ORMを長所と短所がはっきりした抽象化として扱うべきだという方向に近い
オブジェクトモデルとリレーショナルデータベースの違い
- ORMが扱うオブジェクトは、他のノードを指す有向グラフに近い
- リレーショナルデータベースのテーブルは、共有キーを通じてデータが双方向につながる無向グラフに近い
- ORMが無向グラフをまねることはできるが、設定や動作が常に単純とは限らない
UserオブジェクトにPosts配列が含まれない場合がある
Posts配列内のエンティティが、同じUserオブジェクトへ戻る参照を持たない場合がある
- 戻り参照があっても、同じオブジェクトではなく複製されたオブジェクトである場合がある
- たとえばORMは、ユーザーの投稿を配列として返しつつ、各投稿に著者としてのユーザーへの逆参照を含めない場合がある
- この不一致はORMに対する良い学術的批判ではあるが、実運用でより大きな問題は具体的な使い方に現れる
SRPとSOCをめぐる議論
-
単一責任原則(SRP)
- ORMは**単一責任原則(SRP)**に違反するという批判を受ける
- 1つのクラスやレイヤーに複数の役割を同時に担わせる可能性があるため
- データベースとトランザクションの実行
- レコードの表現
- リレーションの定義
- マイグレーションの生成と実行
- しかしORMの目的そのものが、互いに異なる2つのデータパラダイムをつなぐことなので、一部の原則違反だけでORMを否定するのは難しい
-
関心の分離(SOC)
- ORMは**関心の分離(SOC)**の原則も曖昧にする可能性がある
- SOCは、インフラ構成要素が1つの関心事だけを持つべきだという原則である
- ORMはデータベース管理をバックエンド側へ移すため、SOC違反と見なす余地がある
- ただし今日のインフラ構成要素やコーディングパターンは、パフォーマンス、レイテンシ、コード整理のために複数の役割を結合することもある
- OLAPデータベース内のCPUアグリゲーター
- エッジのバックエンド・フロントエンド
- モノレポ
パフォーマンス問題はORMよりも使い方でよく起きる
- ORMが常に非効率だという批判はおおむね誤りである
- 多くのORMは、開発者が考えるよりはるかに効率的に動作できる
- 問題は、ORMがJavaScriptやRubyのようなホスト言語のロジックでデータを組み合わせやすくしてしまう点にある
- TypeORMの例では、特定企業の著者を先に取得し、その後に著者ごとの投稿を再取得し、各投稿を個別に保存する方法が悪いパターンとして扱われている
- より良い方法は、TypeORMの
createQueryBuilder()で単一の更新クエリを構成すること
- Lagoの請求SQLリファクタリングでも、Active Record版と生SQL版の間にパフォーマンス差はなかった
- Active Recordのデータ結合機能を多用していたため、既存のクエリもすでに最適化された状態だった
- 生SQLへの書き換えは、パフォーマンス改善というより可視性の確保に近かった
ORMが実際に遅くなり得る場合
- ORMが常に生SQLと同じくらい効率的なわけではなく、一部の状況ではかなり非効率になることがある
- 1つ目の問題は、ORMがクエリ結果をオブジェクトに変換する際に大きな計算オーバーヘッドを生む可能性がある点
- 2つ目の問題は、1対多または多対多のリレーションをたどりながら、データベースへ複数回の往復リクエストを送る場合
- このパターンはN+1問題と呼ばれる
- 元のクエリ1件にN件のサブクエリが追加される
- Prismaの例では、ユーザー、投稿、コメントをネストして取得する際、各コメントについて新しいデータベースリクエストが発生することがある
- N+1はORMでよく現れるが、**データローダー(data loader)**を使えば、N+1件のクエリを2件のクエリにまとめられる
- ORMの一般的な問題の多くは、ORMの機能を十分に活用すれば避けられる
より大きな問題は可視性とデバッグ
- ORMの最大の弱点は可視性である
- ORMは事実上クエリビルダーであるため、明らかなプリミティブ型エラーのような場合を除けば、最終的なエラー伝達者ではない
- SQLエラーが返されると、ORMがそれを解釈してユーザーに伝え直す必要がある
- Active Recordはこの点で難しさがあり、Lagoが請求サブスクリプションクエリをリファクタリングした理由もここにある
- 期待と異なる結果が出ると、次の作業を繰り返す必要があった
- レンダリングされたSQLクエリを確認する
- そのSQLを再実行する
- SQLエラーをActive Recordの変更へ再び反映する
- この往復作業は、SQLデータベースと直接やり取りしないためにActive Recordを使っていた本来の目的を弱めてしまう
実務上の判断
- ORMは正しく使えば、生SQLに近い効率を出せる
- 多くの問題は、ORMのSQL風機能を使わず、ホスト言語のロジック構造に過度に依存するときに発生する
- 大きなクエリが開発者を苦しめ、デバッグを妨げるなら、生SQLクエリへ移すことが良い投資になる場合がある
- ほとんどのORMは、ORM内部でSQLクエリを実行する機能を提供している
1件のコメント
Hacker Newsの意見
JavaでHibernateが出たばかりの頃からORMを使ってきたが、ずっといまいちだった
「別のデータベースに乗り換えられる」という利点は、今となってはほとんど戯言に近く、実際には誰もそういう使い方をしていない
「SQLを知らなくてもよい」という話も間違っていて、長く続く非trivialなアプリは、結局個々のクエリを文字列レベルで手直しすることになる
データ層はクエリを1つずつ文字列SQLとして作り、補間する方式が正しく、raw JDBCに近いほどよいと思う
ORMが「ドメインモデル」を支援するという主張もよくない理由で、そのドメインモデルは常にロジックの空っぽな貧弱なドメインモデルになるからだ
ORM、XML、アノテーション、生成されたSQLのデバッグなどに浪費した時間を思うと泣きたくなる
簡単な部分はすでに簡単なのであまり興味がなく、むしろクエリが規模にぶつかって崩れる最悪の瞬間に、ORM自体と格闘するせいで作業が100万倍難しくなったことが何度もあった
クエリビルダーはORMと完全に同じではないが、https://gajus.medium.com/stop-using-knex-js-and-earn-30-bf41... の記事が好きで、Slonik系の技術のファンになった
結果をオブジェクトに自動バインドし、強い型検査をしてくれるなど、欲しい機能の90%を提供しながら、ただ普通のSQLを書かせてくれる
SQLが神秘的に難しい言語だという考えも理解できない。古くて欠点はあるが、ORMツールが作った独自のクエリ言語を学ぶくらいならSQLを学ぶほうがよい
当時はこうした批判を口にすると、他の開発者から見下されがちだった
今でも開発者として無理に使わされるツールや手法の中にはアンチパターンが多く、業界の不幸な現実は、広く使われているツールのかなりの数が微妙で、アンチパターンを助長していることだ
技術業界には意思決定権を持つ、つながりの強い少数の開発者や元開発者たちがいて、彼らがいまいちだと感じる
長所と短所の比率が悪くないORMもあり、特にPython方面はよい
Django ORMは不格好で性能もいまいちだが、統合がよく、実用的で生産性が高く、使い心地もよい
Peeweeは小さなパッケージでORMを提供しており、大した機能が必要なく、面倒なときに小さなプログラムを楽しく書かせてくれる
SQLAlchemyはより多くの投資が必要だが非常に柔軟で、きれいなSQLを生成し、動作も正確だ
オブジェクト指向パラダイムを望まず、慣用的なSQLの動作をPythonで抽象化して表現したい場合のために、より低レベルのクエリビルダーも公開している
結局は投資対効果を見る、普通のエンジニアリング上の判断になる
10年以上やってきたが、Hibernateのドキュメントを5分読んで、その状況で指示されているとおりにすれば、それ以上うまく処理できないクエリはなかった
すべての開発者が3値論理表とCOBOL式の関数名を暗記すべきだと信じているSQLファンボーイたちが、肝心のHibernateドキュメントを読む手間はかけたがらないように見える
ORMはOLTPワークロード向けであってOLAP向けではなく、Hibernateの創始者の1人もそう言ったことがある
主な効用は、長くてエラーを起こしやすいINSERT/UPDATEクエリを直接書かなくて済むことと、SQL行をオブジェクトに自動マッピングしてくれることだけだ
rawな方式にすると、その多くを再実装することになるので、これもゼロサムゲームではない
十分に複雑なアプリでは、アプリの複数の部分が一緒にクエリを遅延合成できるクエリビルダーのようなものが、いずれ生まれる
データをメモリ上で結合・フィルタリング・ソートする代わりに、クエリとして組み立てる構造になる
ORMはそれを簡単にできるツールだが、気づかないうちにN+1クエリを作りやすく、自分の足を撃つことにもなり得る
どんなツールでもそうだが、実際のユースケースとともにトレードオフを検討すべきであり、SQL文の文字列を直接操作するやり方は、どんな場合でも推奨しない
それでもその違いを無視して、ORMは良い、あるいは悪いと絶対的に語る人は常にいる
そういうものは注目を集めるための論争的な態度として受け流すのがよく、「ORMには利点があるが、どこで有効なのかを知り、利用上の問題が利点を上回らないようにすべきだ」というところから始まる議論に集中したい
データが必要ならデータアクセス層に行けばよく、必要なものが提供されていないなら、新しいリポジトリやプロバイダーなど、パターンに合ったものを作ればよい
ORMであれ自作のクエリ合成器であれ、アプリが「十分に複雑なのでこれが必要だ」と錯覚するほどになっているなら、すでに安定して使うには複雑すぎる状態になっている
結局、間違ったレイヤーでビルダーを評価したり、結果を走査しているうちにN+1クエリを作っていることを見落としたりする
ORMは不要
LINQは厳密にはORMではなく、強く型付けされたLINQ文を作ると式ツリーに変換され、さらにクエリプロバイダーがクエリに変換する
開発中はモデル内でのORMの可読性を好むが、必要なときにはN+1やその他の非効率を注意深く監視し、最適化する
「必要なとき」の境界は明確ではないが、早すぎる最適化はしないようにしている
新しいプロジェクトはデータベースから始めることが多く、コーディング前にファクトリ、シーダー、raw SQLクエリでデータモデルの感触をつかむ
LaravelのEloquentのようなORMには、N+1を解決し遅延ロードを行う優れたメソッドがあるが、常にトレードオフはある
遅延クエリ合成を許すライブラリの代わりに、クエリを直接書くと、ライブラリがコンパイル時にそのクエリ用のコードを生成する
例えば
getUser: SELECT * FROM users WHERE id = ?のようなクエリを書くと、GetUserQueryクラスとgetUser(id: String): GetUserQueryResultのようなメソッドを作ってくれる形で、このモデルのほうがずっと良いと思うORMが嫌いな主な理由は、現代のSQLエンジンを誇張された愚かなビット置き場のように扱う傾向があるから
CTE、LATERAL join、RETURNING句が処理を大幅に単純化したり、データ不整合の可能性をなくしたりできるのに、ORMはたいてい基本テーブルとビューを事前定義されたオブジェクトに単純マッピングする程度にとどまる
ORMがテーブルまで作ると、さらに悪くなる
SQLは本質的にデータ抽出ツールであると同時に変換言語でもあるのに、ORMはこの面をほとんど無視するため、多くの開発者がSQLにINSERT/SELECT/UPDATE/DELETE以外に何があるのかを知らないままになる
ピボットテーブル、時間問い合わせ、CUBE/ROLLUP、ウィンドウ関数、集合返却関数、マテリアライズドビュー、外部テーブル、JSON処理、日付処理、排他制約、範囲・区間・ドメインのような型、行レベルセキュリティ、MERGEなどがある
完全な工具倉庫を持っているのに、誰かがハンマー、ドライバー、金ノコを1つずつ渡して「これで十分だ」と説得しているようなもので、ほんの数メートル横にテーブルソー、ルーター、サンダー、くさびセットがあることも知らずにキャリアを過ごすことになる
ORMはたいてい単純なCRUD文を中心に考えさせるが、おそらくそのほうがよく、高度な機能は控えめに使うべきだと思う
それでもDBを使うことになれば、意図的にDB機能のごく小さなサブセットだけを使おうとする
「コード」は制御できる領域であり、「データベース」はエラーが後になって遅い統合テストのような場所で現れる、危険で手の届きにくい領域だと見ているから
DB内で起こるロジックは低レベルテストの範囲外にあるので怖く、おそらく
select-from-selectクエリすら使わないと思う工具倉庫の大部分は喜んで置いていく
基本的なデータ整合性の保護機構を再発明するために、時間の大半を精巧な回避策作りに費やすことになった
自分だけのデータベースを作るのは学習用なら楽しいかもしれないが、本番アプリでは違う
ActiveRecordはいつも良いと感じていた
一部の例外状況はあるが80/20のポイントをうまく押さえており、必要ならSQLに逃げられ、ドメインロジックを整理してくれるライフサイクルフックも優秀で理解しやすい
すべてを「バンドル」の裏に隠したり、独自のクエリ言語を導入したりしない実用的なORMは良い
ORM問題を完全に「解決」しようとするORMは扱いにくい
ORMはこう作られるべき
ただし、正しく使うにはSQLについての十分な事前知識が必要
人々がSQLを学ばず、ARが生成するクエリとその限界を理解していないときに問題が起きる
前提が間違っている。ORMが一度でも「有害なアイデア」だったわけではない。
一緒に仕事をするのがひどくつらいORMもあるが、だからといってジャンル全体が悪い考えだという意味ではない。
SQLAlchemyはSQLという概念そのものを再発明しようとしないので、何年も楽しく使ってきた。
代わりに、クエリを作って受け渡すための便利なレイヤーを提供し、細かな詳細をきちんと処理してくれるので、自分でいじり回さずに済む。
最近Rustを学びながら、単純なCRUDを扱う定型コードを自分で書くのが苦痛で時間の無駄のように感じている。
SQLAlchemy ORMは、より複雑だったり最適化しようとするクエリでは少し邪魔になることもあるが、そういう場合はいつでも直接SQLに降りられる。
何事もそうだが、うまく合う状況とそうでない状況があり、技術は簡単に乱用・誤用され得る。
例えば.NETのEntity FrameworkのようなORMは、特にデータベースファーストで作業する場合、大きな資産になり得る。
数百のテーブルと正しい外部キー関係が備わったDBなら、ORMは複雑なスキーマを探索し、変更時にコンパイル時チェックを提供するのに役立つ。
組み込みSQLは性能上の利点に見える諸刃の剣だが、特にJOINでは、ハードコードされた冗長なSQLを保守するのは非常に難しい。
テーブルが5〜10個のときではなく、数百個あるときの話だ。
誰かを説得するつもりはないが、ベストプラクティスについてインターネットで読んだことをそのまま信じるべきではない。
人々は互いにまったく別の対象について話しているかもしれないし、どんな技術を使ってもひどい開発者はいるものだ。
ORMやほかのDBインターフェイスを使わなければ、結局CRUDのようなものについて自分で一つ書いて保守することになる。
そしてその中には、奇妙なバグ、エッジケース、セキュリティホールが入り込む可能性が高い。
SOLIDのような原則を大学の授業でソフトウェア設計原則として教えることにも問題がある。
学界では時間、保守性、お金、労力といった制約の重要度が低いので筋は通るが、新卒が現場に入ると非常に悪い衝突が起きる。
「完璧な」解決策を探して一日中ヤクの毛刈りをし、車輪を再発明することに慣れているからだ。
それを抑えられる十分に有能なシニア/リード開発者がいないと、非常に興味深い製品、予算超過、大火事、またはその組み合わせが生まれる。
さらに悪いのは、学界で作られ、学界のために設計されたものが一般に放たれるときだ。
代表的な良い/悪い例がOpenStackで、CERNと数百の大学の特殊な環境を支えられるほど多くのつまみや調整点があり、構成要素は狂ったようにモジュール化されているが、あまりにビザンチン的で、始めるのがほぼ不可能だ。
ORMはデータベースを苦労なく交換してくれるものではないし、生成するSQLを学び理解する必要をなくしてくれるものでもなく、スキーマを滑らかにしてくれるものでもない。
それでも使う理由は、いずれにせよ自分で再現することになる標準的な抽象化だからだ。
DBアクセスのキャッシュを可能にしてくれるが、手書きSQLでは規模が大きくなるほど驚くほど難しい。
標準化されたツールとベストプラクティスが付いてくるので、N+1のような問題を関係ないものにできる。
ORMの主な問題は誤った期待と悪いORMだ。
ORMの価値を知りたければ、ORMなしで書かれたプロジェクトを開いて、何かを変更してみればよい。
https://news.ycombinator.com/item?id=36498583 参照。
有用な仕事が進まなかったり、素早く汚いアプローチより20〜100倍長くかかったりした。
最悪だったのは、熟練したHTMLデザイナーのチームが関数型プログラミングを学ぶよう強いられたケースだった。
バックエンド設定ウィザードは現代工学と優雅な設計の驚異だったが、10人が1年かけて作ったものだった。
VCマネーは人をおかしくする。
SQLという言語は非常に複雑で、きちんとしたフル機能のクエリビルダーを作るのは本当に些細なことではない。
実際のリレーショナルマッピング部分は別としてもそうだ。
SQLは「とにかくその忌々しいものを自分で直接プログラムしろ」が当てはまる事例だと思う
SQLやORMライブラリを学ぶことに大した意味はなく、SQLを直接学んで書くほうがよい
データベースアクセスは、おそらくキャリアを通じてずっと行う根本的な作業なので、実際の技術を直接学ぶのが最善
CSSも似ていて、CSSライブラリを学ぶよりCSSを学んで使うべき
実際、フロントエンド開発のほぼすべての面もそうで、「フォームライブラリ」の代わりにブラウザのフォームAPIを直接扱えばよい
個人的には、SQLはどのORMよりもずっと学びやすかった
間違ったものを学ぶコストを無駄にすべきではない
CSSも同じで、CSSを知っていれば、利便性のためにライブラリを選ぶときに、はるかに柔軟で理解も深くなる
メインフレーム、デスクトップ、クライアント/サーバー、Webなど、さまざまな開発プラットフォームとその上の多様な言語を経ても、SQLは常に共通する糸だった
データ層とその下で何が起きているのかをよく知っている開発者は、より強い開発者だという点には完全に同意する
ORMの癖を学ぶのに時間を使うくらいなら、なぜその時間をSQLの学習に投資しないのか、という古典的な問いがある
SQL経験の複利は、10〜15年後には特定のプラットフォームや言語のORMより価値が高い可能性が大きい
ORMを学んで使う理由はあるが、SQLを学びたくないという理由だけでORMに傾くべきではない
そうすれば、その抽象化に対して何を代償として払っているのかが分かる。もちろん「ゼロコスト抽象化」なら話は別だが
ところが今は、Goの型とのマーシャリング/アンマーシャリングのために、ORMに分類されるであろう
azerのcrudを使っているGo標準ライブラリの
sqlはあまりに基本的なので、オブジェクトのマーシャリング/アンマーシャリングを実質的に自分で書く必要があり、本当に退屈最近ORMを使うとしたら、まさにその用途だけに使うと思う
DapperやDieselのような「中間地点」のマイクロORMが好き
クエリはSQLで書きつつ、強い型付けと結果セットのオブジェクト自動マッピングを得られる
生産性と、完全な機能を備えた「本物の」クエリ言語を同時に得られるわけだ
また、オブジェクトマッパーは読み取り専用クエリにだけ使うというやり方も好きになった
更新はコマンドパターン、つまりストアドプロシージャで行い、それをネイティブメソッドのように見える形にマッピングできる
開発者はSQLが存在しないふりをせず、ただ受け入れるべき
Eloquentはそうした雑務を十分に抽象化してくれるので、ロジックに集中できる
配列とクエリだけの「古い」PHPアプリを、モデルとEloquentを使う構造に書き直しているところだが、完全にゲームチェンジャーだと感じる
以前.NETでSQLiteを使っていたときも、その方向のラッパーライブラリを作った: https://github.com/zmj/sqlite-fast
このスレッドは、ORMのマインドシェア支配力をよく示している
ほぼすべてのコメントで、ORMの暗黙の代替案はアプリコード内の文字列リテラルとしてのraw SQLになっている
すでに6年前から、SQLを1つの言語として使い、クエリをアプリにメソッドとして公開するラッパーを生成するツールがあった
queryfirst、pgtyped、pugsql、sqlcのようなものだ
これはパラダイムシフト、少なくともそうなり得たもので、明らかにより優れた作業方法であることが示されているのに、ORMと変わらないこの議論が酸素をすべて吸い尽くしてしまい、周縁にとどまっている
SQLは言語であり、それも非常に高水準な言語だ
より低水準の言語でSQLをつなぎ合わせようとするのは、「ハンマーを持つとすべての問題が釘に見える」の典型例
例えば、低水準なアセンブリでコーディングしながら大きなシステムとやり取りしていて、そのシステムのドキュメントに入力はJavaScriptでしか表現できないと書かれているとする。その場合、選択肢がある
純粋なアセンブリで何かをつなぎ合わせられる難解なライブラリを持ち込んで
xxx.jsファイルをリポジトリにコミットしないこともできるし、単に.jsファイルとして高水準言語の力を使って表現することもできる使っているSQLクエリについて単一の信頼できる情報源ができ、実際に書いて保守する対象がクエリそのものなので、新しいインデックスを作るような最適化もしやすくなる
raw SQL文字列の上に抽象化を置くのは非常に有用だが、リレーションをオブジェクトのふりをさせるのは、複雑にこじれたやり方だ
TopLinkもおそらくそうだったはず
ストアドプロシージャとローカルエンティティ間のマッピングを処理する方法がこれだった