使えるO/Rマッパーの見分け方

SQL以外直さなくていいはずなのに

某ファミレスの業務システムはこんなデータ構造を持っていた。

商品マスタ = { 商品ID(PK), 商品名, 単価 }
メニューマスタ = { メニューID(PK), メニュー名 }
メニュー別商品 = { 商品ID, メニューID }

メニューマスタには、春夏秋冬の4種類のメニューが登録されている。
メニューと商品の関係は m:n だ。


ある時、一部商品の単価を季節によって変動させることになった。
そこで単価の置き場所を「商品マスタ」から「メニュー別商品」に変更した。
商品マスタの単価は、メニューごとの単価を入力する際のデフォルト値という位置づけで「標準単価」と呼ぶことにした。

商品マスタ = { 商品ID, 商品名, 標準単価 }
メニューマスタ = { メニューID, メニュー名 }
メニュー別商品 = { 商品ID, メニューID, 単価 }

メニューに含まれる商品とその単価を取得するクエリは、この変更の前後で以下のように変わる。

select 商品マスタ.商品名, 商品マスタ.単価
from 商品マスタ
inner join メニュー別商品 on メニュー別商品.商品ID=商品マスタ.商品ID
inner join メニューマスタ on メニューマスタ.メニューID=メニュー別商品.メニューID
where メニューマスタ.メニューID=?

select 商品マスタ.商品名, メニュー別商品.単価
<以下省略>

結果表の形はまったく同じなので、アプリケーションの修正点はSQLだけ、のはすだ。
ところが、ある種のO/Rマッパーは、いろんなところの修正を要求する。

  • メニュー別商品クラスを新規作成する*1
  • メニュー別商品クラスに「単価」プロパティを追加する。
  • マッピングの定義ファイルを変更する。
  • 商品クラスのプロパティとして単価を参照しているところを、メニュー別商品クラスのプロパティを参照するように書き換える。例えばO/Rマッパーの出力をJSPフォワードしているなら、こんな修正が必要かもしれない:
<!-- 単価は「商品」オブジェクトのプロパティ -->
<bean:write name="item" property="price" />
    ↓
<!-- 単価は「商品」オブジェクトのプロパティ「メニュー別商品」のプロパティ -->
<bean:write name="item" property="menuItem.price" />

こんな修正は、大した手間ではない。
が、何かおかしいなあと感じる。
SQLでは同じ形のものを返せるのに、O/Rマッパー経由でRDBにアクセスすると同じものが返せなくなる。これは、問題のO/Rマッパーが、結果表をラップせずに、生の基底テーブルをラップしているから起きる問題だ。


現在、O/Rマッパーの出力を直接JSPフォワードするのは、あまり良い設計とは考えられていない。
サービス層でDTOに詰め替えるのがよい設計らしい。
そのようなサービス層を作ってあれば、10画面のJSPを修正しなくてはならなかったところ、サービス層の1メソッドを直しただけで修正が完了する ...
のだが、やっぱりおかしいなあと感じる。
それは、SQLで射影を切り出せば済むことを、わざわざ人力でやっているのではないか。

まとめ

オブジェクトを特定のSQLにマップできるO/Rマッパーは便利だが、基底テーブルにしかマップできないO/Rマッパーは、書かなくていいプログラムを書かされる可能性がある。
その面倒を省いてくれる周辺ツールがあるか、チェックした方がよい。

基底テーブルにしかマップできないO/Rマッパー経由でRDBを扱う時は、オブジェクト指向DBにアクセスしているつもりになった方がよいのかもしれない。

  • 射影を切り出せない
  • 集合演算(unionなど)が使えない

というデータベースは、RDBではない。
あなたが使っているO/Rマッパーが union(, except, intersect)を含むクエリを発行できないなら、
あるいは、単価の置き場所が「商品マスタ」から「メニュー別商品」に移ったときに、クエリが返すオブジェクトグラフの形も変わってしまうなら、あなたはRDBの上にアプリケーションを構築しているのではない。仮想的なオブジェクト指向DBの上にアプリケーションを構築しているのだ。
そのオブジェクト指向DBは、十分な機能を提供しているだろうか?ドメインモデリングの結果をそのまま実装できるだろうか?
RDBRDBとして使うより、本当にコード量が減っているだろうか?メンテ時の作業量が少ないアプリケーションになっているだろうか?

*1:交差エンティティをオブジェクトの相互参照に変換して、オブジェクトにマップしないO/Rマッパーも多い。その場合は新規作成が必要