今さら15年前の『エンターラプライズRails』を読んでみた
『エンタープライズRails』はDan Chak氏による、Railsをエンタープライズ環境で運用するための設計術を解説した書籍であり、邦訳は2009年に出版された。
今更15年以上前の本を読むことに意味はあるのか、と思われるかもしれないが、本書で語られた多くの原則やアイデアが、2025年現在のRailsコミュニティや技術動向にどのように受け入れられたのかを、キャッシュ、ActiveRecord、SOA、API設計といった分野ごとに整理してみることで見えてくるものがあるのではないかと思い、メモを残すことにした。
ちなみに、本書の内容はRails 2系を前提としている。
キャッシュについて
View, Materialized View
キャッシュについて主にViewやMaterialized Viewの利用が提案されていた。
このアプローチは、現在のRails開発ではあまり活用されていない印象がある。代わりに、非正規化した物理テーブルとして実装されたり、RedisやElasticSearchなどの外部アプリケーションに置換されていることが多い。
一方で、ScenicのようにMaterialized Viewをサポートするgemも存在しており、一定の利用はある様子。
不採用の理由として考えられるのはSQL側へのロジック流出だろう。また、速度を求めた場合には、Redisなどに特定のアクションでだけ使われるデータを保存し、それをそのまま利用するケースの方が多い。結局、いろいろな箇所で変更をObserveして更新するよりも、使用時にキャッシュしておく方が導入しやすかったのかもしれない。
expiryやdirtyといったカラムを使って変更検知したり、SELECT時にトリガーを使うことで必要なレコードだけ更新したり、みたいなのは面白かったが1つのMaterializedViewを作るのに5個ぐらいViewが出てきたりしていて流石に…
自分としてもこの選択肢はあまり検討してこなかったが、再考する価値があると感じた。たとえば、after_createやafter_commitを使って、更新タイミングをトランザクション内外で調整することも可能であるのはメリットだろう。
特にCQS, EventSourcing, Immutable DataModelのProjectionなど、Rails wayから踏み出す際にRead専用のモデルを設けるのは有効かもしれない。
ただ、フラグメントキャッシュなどと比べて、よりリクエストから遠い層にキャッシュが存在するため、導入にはキャッシュそのものと事業に対する戦略的な理解が求められると感じた。
Rails側でのキャッシュ管理
上記のMaterialized Viewや論理モデルのオブジェクトキャッシュの管理方法に関してObserverやシングルトンなCacheManagerの利用が提案されていた。
これは今でも使えそうかなと思った。一部CacheManagerがクラス変数を通じて再構築が必要なオブジェクトを保持していたりもしていて驚いたが、UnicornやPumaのようなマルチスレッドなアプリケーションサーバーが一般的ではなかった時代の名残だろう。Rails 2時点では、FWそのものがスレッドセーフでもなかったらしい。
また、キャッシュの更新戦略として以下の3つが提案されていた:
- 再構築中は古いオブジェクトを返し続ける
- 再構築中はリクエストを待たせ、完成後に新しいオブジェクトを返す
- 各リクエストごとに再構築が完了するまで待たせる
ActiveRecordについて
外部キー制約、CHECK制約を使ってDBの整合性を保つ
これは今でも有効なアプローチであり、アプリケーションよりもDBの方が寿命が長いので固めておけというのは定説だ。
DHHなんかはもうpresence validationみたいな単純なものはCHECK制約だけで十分だ、みたいなことも言ってたりしている。
実際外部キー制約のないサービスのメンテなんてうんざりだ、みたいな感じで転職していった人も見たので、ここはサービス開発当初からしっかりと考慮しておくべきだろう。
MTI(Multiple Table Inheritance)
STIから一歩進んだ実装としてMTIが提案されていた。
Railsではdelegated_typesを通じて、複数テーブル継承(MTI)が公式にサポートされるようになったので、この点は現在も有効なアプローチだと言えそうだ。
複合主キー
idではなく、複合主キーを利用することが提案されていた。一応公式でもサポートされている様子。
3NF+ドメインキー+idを使えば中間テーブルを跨いでも直接レコードを参照できるという主張もあったが、実際には管理コストや制約処理の煩雑さに比して、得られるメリットは限定的だと感じた。
特に、削除や更新時の外部キー制約の順序管理が複雑になったり、トランザクション内での制約の一時的な解除が難しくなることが多い。また、それがそれらがユニークである、という前提が崩れることもままあるので、複合主キーは避けられてきたのかなと。
ただ、Railsが画一的なidを前提とする設計であることに起因する弊害もあるとは思った。
特にDDDの文脈でそのModelがEntityか否かを判断する方法がなく、updateすべきか、replaceしてもよいのかなどの判断が難しい。個人的には、Entityであるならばその意図をコメントに記述する程度で運用するのが現実的かと思う。
ドメインテーブルをレコードにする
郵便局などのマスタデータはテーブルとして管理すべき、という提案がされていた。
個人的にはクエリ発行のコストや本番との差分管理を考えるとあまり賛成できない。
レコードが少ない場合はActiveHashを利用すれば、ほとんどの場合はそれで十分であり、DBアクセスも発生しないため高速に動作する。さらに、コード管理のため定数参照や訳文の管理も行いやすい。
ただ、運用部門が定期的に変更を行う場合は、DBテーブル化したほうがいいと思う場面があったので運用から逆算して考えるのが大事そうだと思った。
ActiveRecordをハッシュに置き換えて高速化する
ARインスタンス生成のコストが高いため、表示専用であればハッシュで代替するという提案があった。
これは一見乱暴に見えるが、キャッシュやビューテーブルと組み合わせた場合見え方が変わってくるかなと。
そもそもARの生成がボトルネックとなるのは以下のケースだと思われる
- 一覧表示で多くのレコードを使用する場合
- バッチ処理で大量のレコードを扱う場合
1の場合はそもそもそんな大量のレコードを表示する画面設計が適切でないことが多い気がする。また、Viewは変更の激しい部分なのでキャッシュをそれに合わせて変更するコストを考えるとあまり現実的ではないのでは。
2の場合は、バッチは単機能で特定のカラムだけを要求していることが多いし、扱うレコード数から考えてActiveRecordを使うのはオーバーヘッドが大きいので、ハッシュや他の軽量なデータ構造を利用するのは有効な手段だろう。
SOA
当時はSOAPやXMLが主流であり、ActionServiceのような仕組みがRails 2系には存在していたらしい。
現在ではRESTfulなAPI設計が主流となり、JSONがデファクトスタンダードとなっている。
一方でgRPCやGraphQLのようなより柔軟なAPI呼び出しの手段も採用されるようになっており、本書で述べられているようなSOAの原則は、現在でも有効な考え方だと思った。
SOAの4原則
- 耐障害性:リクエストはステートレスかつ任意のインスタンスで処理可能であるべき。
- 契約の厳守:APIの変更はメジャーバージョン時のみ許容されるべき。
- 発見可能性:WSDL/WADLなど機械可読な仕様が必要。
- 標準との整合性:業界標準の技術で実装し、多様なクライアントの接続を保証すべき。
API設計のベストプラクティス
-
必要なものをすべて送る
一度の呼び出しでクライアントが必要とするすべてのデータを取得する方が効率的 -
ラウンドトリップを制限する
クライアントが必要なデータを取得するために複数のリクエストを連鎖させなければならない状況を避けるべき -
並列化の機会を探す
独立したサービス呼び出しは、すべて同時に発行して結果を待つことができる -
できるだけ送らない
データからサービス呼び出しへ、またはその逆に変換する操作(特にXMLのシリアル化や逆シリアル化)はコストがかかるため、リクエストに必要最小限の情報のみを送ることで、このコストを回避する
その他
初期段階からスケーリングを考慮すべきか
どうせ困るんだから最初からディレクトリ分割やキャッシュを導入しておけ、みたいなことが書かれていたが、これはあんまりかなと。
スタートアップはデフォルトで死んでいるという話、質とスピードは車輪の両輪である、みたいな話から考えると、大事なのはそのプロダクトがどの程度の機能を備えればPMFするのか、を見積もってそのコード量を満たす最小限のアーキテクチャを採用することかと。
が、結局それもおそらく無理なので各フェーズでできる範囲で疎結合にしておいて、分割は後から考える、が現実的かと。
つまり、まず初期段階ではAR依存でスピードを出すために単一モデルにユースケースが集中しないようにする
そのあとは何が課題になっているかによって後付けするアーキテクチャを考える。
たとえばパフォーマンスが問題ならCQSが有効だろうし、ドメインの複雑さが問題ならModelのカラムをValueObjectに分割していくなどの手法が良いだろう。監査が問題ならEventSourcingを導入するのも良い。
チームと依存の問題であればpackwerkなどのモジュラモノリスに移行していく、が現在だと有力な選択肢かと思われる。
総評
細かい部分ではvalidationの書き方やDB制約をRSpecで評価している点など、ツッコミどころも多かったが、RDBを用いたキャッシュ戦略や、SOAを通じて得られる現代的な設計手法への示唆が多く、再考の契機となる良書だと感じた。
メンバーにも読ませるかと言われるとそうでもないが、過去のRailsの歴史や設計思想を知りたい人には有用な資料だと思う。