YAPC::Kyoto 2023 で ORM について喋ってきた

資料は こちら です。

背景

アーキテクチャ的に何かを足したいとき、我々はチーム開発を行っているのだから、チームの共通認識を変えるということになる。認知負荷が高い場合は提案を拒否されてしまうので、認知負荷をできる限り小さくして導入したい。つまり差分の最小化です。*1

現在のコードベースと、入れたいアーキテクチャを対比させつつ、こう導入するのがベストと見切るところが今回のトークの面白ポイントです。

PoEAA のデータソースのアーキテクチャに関するパターン

PoEAA は 20 年前の本なので、当時の開発風景を想像できる人と会話しながら読むと良いです。エリックエヴァンスの DDD 本も似た時期ですね。2002 年は Java 1.4 がリリースされた頃。デザインパターンUMLXML が流行っていた。ライブラリのパッケージマネージャやセントラルリポジトリがまだ無い。*2

再利用性があり生産性が高いらしい「オブジェクト指向プログラミング」に全ての夢を託していたあの時代。また、もちろん Rails も無いので migration (アプリケーションエンジニアが DB を気軽に変更し続ける) という文化もまだ発見されていない。

PoEAA では、アプリケーションはドメインを表現するようにしたい。データベースは容易には変更できないので、ドメインと結合させない、DataMapper パターンに自然となっていくだろう、という語り口で書かれている。その後、現実は密結合で加速する方が認知負荷が低くて扱いやすい、DB のリファクタリングをどんどんやる、そもそもドメインとデータソースを分離するのは割と無理筋、という方向になったっぽいと思う(観測範囲)。

データソースのアーキテクチャに関するパターン 4 つを構造で分類して説明するというアイディアは PHP/アクティブレコードってなに? から借りた。*3

greppability

Sample::Repository::User みたいに、各テーブルに対応するクラスを作ることの是非かぁ。そもそも世の中を席巻している ActiveRecord パターンはテーブルに対応するクラスを作るので、そこで違和感を持つことは無かったなぁ。

ただテーブル名を文字列から定数にするだけではなく、ちょっと複雑なクエリ置き場を作る。クラスにしたことで、こういうものはココに書くと決められる、という意味を付けられたんじゃないか。まさに TableDataGateway として整理した。

greppability に関しては、現実世界のオペレーションとして「どこからこのテーブルが触られているのか」というのは時々眺める必要がある。謎データが入った原因を探したり、急に垂直分散する必要が出たり。*4 少しでも日々の作業が楽になるように多少の工夫はしたい。

SQL が分からなくならない?

Perl の DB 周りのライブラリには基本的にどこで発行した SQL なのかをコメントで残す機能がある *5 ので、スロークエリの検知や集計さえできていれば発生した問題には対応できる。 また、集計や検知するためのサービスが現代では整っていて、自前で pt-query-digest 等で集計しなくても Performance Insight が教えてくれるので、頑張らなくても見つけられるんじゃないかと思っています。

今は移行直後なのでみんな SQL への脳内マッピングができる状態だけど、「ORM しか触ったことのない人」が増えた世界だと「発行される SQL を見てくれ!!」と言いたくなりそうですね。例えば Devel::KYTProf を入れて流れる SQL を眺めながら開発するよう働きかけるとか、それをやってもログだと眺める人が少ないのでブラウザ上にどうにかして表示するとかが必要になるかもしれません。

そもそも問題が発生しないようにするのは難しいよねー……。工夫はして防ぐけど、たまにやらかしが出うる、勝ち目の少ない戦いっぽいと思う。

ドメイン

質疑もあった。(まとめてくれている id:stefafafan は本当に助かる)

低レベルな API としての SQL、もうちょっとだけ高レベルな (CRUD だけを司る) TableDataGateway、もうちょっと高レベルな (ある程度ドメインロジックも持つ) ActiveRecord、更に高レベルな (テーブルと疎結合になってドメインそのものを表す) DataMapper パターンとあって、DDD のドメインは DataMapper パターンのドメインが一番近いんですよね。

たぶん質問は「Repository」という言葉に引っ張られたのだと思う。DDD のリポジトリ (ドメインとデータストアとのマッピングを行う、まさに DataMapper のこと) ではなく、TableDataGateway(特定のテーブルに対する CRUD を集めたもの)に Sample::Repository::User という名前を付けました。高レベルなものは低レベルのものを組み合わせて実現するので、やればできますが、僕らのアーキテクチャだと既にもう一個上に置いてある Service 層をドメインロジック置き場としているので、新たなドメインモデルを作る理由が無かった、が回答になりました。

これは ドメインモデル貧血症 なのではないかという指摘もありそうですね。その通りと思います。困るようになったら改善するかもしれません。が、僕は 95% のものはテーブルと紐付いたドメインモデルで十分と思っているので、必要なところでだけどうにかする、をやりそう。

話せなかった内容

使用メモリが倍増したのでリリースして慌てて取り下げたとか、JSON を BLOB カラムに入れている場合があって encode_utf8 で一度ハマったとか、トランザクション境界が合ってなくて導入しづらいものがあったとか、入れる上でのトラブルが一通りあるんだけど、あのペースで喋っても時間いっぱいだったので入れられなかった。

Repository や Row を扱いやすくするための工夫や、ライブラリに送った PR についても話したかったけどこれも時間の関係で見送り。具体的には Row をトップレベルの名前空間に置いたのは、特にやって気持ちが楽になった工夫です。タイプ数が多いと普段使いしづらい。ドメインオブジェクトであり、普段から触るものだと認識しやすくするためには、浅いところに置くのが望ましいと思っています。

まとめ

  • 対象領域を整理しよう
    • 今回は ORM を PoEAA の語彙で 4 パターンに、さらに 2 軸で整理した
  • 自分のコードベースを見極めよう
    • どのパターンに近いのか、例外的に扱っているところはあるか等
    • どうあるべきかを考えることになるので、自然とアーキテクチャと仲良くなる
    • アーキテクチャが綺麗だと例外が少ない
  • ギャップが少なくなるようにして導入に持ち込もう
    • 感覚じゃなく、整理して、だからこれが最適解だ、を突きつける
    • 今までとのジャンプアップの大きさは注意しよう
      • 大きすぎると認知負荷が高すぎて導入できない
  • いちど整理すると面白副作用が出てくるのでオススメ
    • 混沌だと手が出せなかったが、整理済みなら改善できる
      • 認知負荷を下げるとハードルが下がるらしい

参考資料とか

*1:なんでも差分最小が良いわけではなく、理想と現実のギャップを捉えた上で、落としどころを探すことになる

*2:Maven も無く Ant で入れていたと思う

*3:のだけれど、今見ると 403 だなー? :thinking_face:

*4:ISUCON じゃない現実でもそういうことはある

*5:Teng の場合は https://github.com/nekokak/p5-Teng/blob/0.33/lib/Teng.pm#L272-L287