O/Rマッピングで緩和されるインピーダンスミスマッチには静的と動的の側面がある
一般的な業務アプリケーションではデータを永続化するために、RDBMS(関係データベース管理システム)を利用します。RDBMSでは大量のデータを効率的に検索したり、集約してレポートを作ったりすることが得意ですし、一般的に業務システムで求められるトランザクションのACID特性*1を満たすことも容易です。また、適切にテーブル設計の正規化を行うことにより、運用面においてデータの管理コストを下げることもできます。最近ではスケーラビリティの問題などもあり、RDBMS以外のデータベースについても注目されるようになってきていますが、今後も業務アプリケーションの主流としてRDBMSは使われていくだろうと思われます。
従って、Javaなどのオブジェクト指向言語で開発を行い、DDDのようなオブジェクト指向の設計技法を利用する場合に必ず考えなくてはならない問題は、オブジェクト指向と関係モデルとのインピーダンスダンスミスマッチにいかに対処するかという問題を考えてなくてはなりません。
DDDにおいてRDBMSとのインピーダンスミスマッチをどう扱うべきか? - Togetter
インピーダンスミスマッチング - 都元ダイスケ IT-PRESS
RDBMSの関係モデルとオブジェクトモデルは、お互いに水と油のような関係のところもあり、一見似ているところもあるのですが、うまく混じり合わないところがあり、また、どちらか一方のモデルに適合させてもうまくいかない傾向があります。しかし、逆にアプリ層とデータ層を完全に分離するためにはマッピングのために相当のコストがかかります。
もちろん、ソフトウェアの世界で銀の弾丸というものはないのですが、最近は以前と違い、JPAに代表されるような本格的なO/Rマッピングのしくみが標準化されており、安価に利用することができます。こうしたしくみを正しく理解して使いこなすことで、両者のインピーダンスミスマッチを軽減させることができます。
ここでは、そもそもO/Rマッピングフレームワークとな何なのか、また、それによってどのようにインピーダンスミスマッチが緩和できるのかについて、簡単にまとめてみたいと思います。
O/Rマッピングフレームワークの分類
実は、O/Rマッピングという用語はかなり幅広い意味で利用されており、それが何をするものなのかを正しく理解していないと混乱が起きる場合があります。大別すると以下の通り分類できます。
部分か完全かの違いは後から説明するようにUnit Of Workパターンを実装して状態の更新に対する動的なインピーダンスダンスミスマッチの解決を行うかどうかの違いで分けています。もちろん、これらの分類のうち、どれが優れていて、どれが劣っているということは絶対的には決まりません。それぞれについては、以下のような特徴があるため、プロジェクトの目的によって使い分ける(あるいは併用する)ということが大切です。
分類 | 長所 | 短所 | 向いているシステム |
---|---|---|---|
SQL中心の マッピング |
レガシースキーマにも 柔軟に対処できる。RDBMSの パワーをフルに活用できる。 |
単純なケースでは似たような SQLを大量に作成することによる 生産性や保守性低下。 オブジェクト指向設計が 困難となることがある。 |
大量データのバッチ処理、 帳票作成処理など。 |
静的 O/Rマッピング |
単純なCRUD処理では生産性が高い。 | 複雑な業務ロジックや データ構造に対処することが困難。 一般に大量データ処理は苦手。 |
マスターデータ管理など、 ロジックが単純な オンラインアプリケーション |
完全 O/Rマッピング |
エンティティに振る舞いを持たせる ことが可能になる。継承や ポリモーフィズムを直接活用できる。 キャッシュによる性能向上。 |
学習コストが非常に高い。 使い方次第では性能面の問題に直面しやすい。 大量データ処理はSQL方式を併用すべき。 |
複雑な業務ドメインを DDDで開発する場合。 データベーススキーマを クラスの構造に合わせて、 自由にリファクタリングできる場合。 |
インピーダンスミスマッチには静的と動的の2種類の側面がある
さて、話をインピーダンスミスマッチの問題に戻しましょう。RDBMSとオブジェクト指向言語とのインピーダンスミスマッチの問題については、
- テーブル構造とクラス構造との静的なミスマッチ
- オブジェクトとテーブルとの更新頻度の違いによる動的なミスマッチ
の2種類が存在します。前者の静的なミスマッチの問題は、オブジェクトモデルと関係モデルとの間での継承関係の有無の違いや、非正規化による粒度の違いのことを言います。一方、後者は多くの場合見過ごされがちな問題なのですが、オブジェクトの状態変更のタイミングとデータベースのUPDATEの頻度の違いについて言及しています。
そして、先ほどのO/Rマッピングフレームワークの分類で言えば
という関係があります。従って、単にインピーダンスミスマッチと言った場合、それがどの部分を指して言っているのかという点を勘違いしないように注意する必要があります。
静的なインピーダンスミスマッチを解決する
通常、多くの人がRDBMSとオブジェクト指向とのインピーダンスミスマッチと聞いてイメージするのは静的なインピーダンスミスマッチの問題なのではないでしょうか。単純なマッピングでは
- データベースのテーブルをエンティティクラスに対応させる
- データベースの各行をエンティティクラスのインスタンスに対応させる
- データベースのカラム属性をエンティティクラスのフィールドに対応させる
といったマッピングを行うことができます。実際、SQL中心のフレームワークやSIer製や内製の自称O/Rマッピングフレームワーク*2ではこのような単純なマッピングを前提としていることがほとんどでしょう。一見このマッピングで問題が無いように見えてしまうかもしれませんが、詳細を考えると
- データベースのテーブルには継承関係が定義できない(継承関係のミスマッチ)
- 値オブジェクトなどを考えると粒度が異なる(粒度のミスマッチ)
- 関係モデルでは本質的に双方向の関連しか定義できない(関連の方向性ミスマッチ)
といったオブジェクト指向設計との相違点がいろいろと出てきます。一つの解決策はデータベース設計を中心で考え、このミスマッチをアプリケーションの設計で制約として受け入れてしまうことです。もちろん、単純なCRUD処理やデータの加工が中心となるアプリケーションではこれで問題とはなりません。*3しかし、ドメインの複雑なビジネスロジックが関与する(実際に多くの業務アプリケーションの場合はそうなのですが)場合、オブジェクト指向プログラミングのメリットを活用できないと、ロジックの重複といった保守性や拡張性の問題を引き起こします。そうしたことが問題となる場合にはO/Rマッピングによる静的なインピーダンスミスマッチの解決策を取り入れることが有用になります。
継承関係のミスマッチ
継承関係のミスマッチに対しては、現在では具体的な解決策がパターンとして知られており、有名なPofEAAでも解説されています。特に継承関係のマッピングのパターンの解説としてはスコットアンブラーの記事が知られています。
Mapping Objects to Relational Databases: O/R Mapping In Detail
この場合、3種類のマッピング方法が知られています。
継承パターン | 説明 | 使うべき時 | JPAでの継承タイプ |
---|---|---|---|
Single Table Inheritance | 継承階層を1テーブルに格納する。 | 商品種別ごとの階層など大量サブクラスが存在する場合。継承がフィールドの違いでなく、振る舞いの違いを中心とする場合に特に有利。JOINやUNIONを利用せず、多態的に親クラスをまとめて検索できるため、一般には性能的に有利。テーブルモデルの観点からは正規化されておらず、NOT NULL制約のないカラムが必要。 | SINGLE_TABLE |
Class Table Inheritance | 抽象クラスも含めて各クラスを別々のテーブルに対応して格納する。 | 正規化された無駄のないテーブル設計が可能で、テーブルモデル自体としては拡張性や保守性が高い。必ずJOINが必要となるため一般には性能面でのコストがかかる。 | JOINED |
Concrete Table Inheritance | 具象クラスのみ別々のテーブルに格納する。 | 具象クラスを別々のテーブルに格納できるためわかりやすいが、多態的な検索や関連の処理はコストがかかる。 | TABLE_PER_CLASS*4 |
JPAを実装した本格的なO/Rマッピングフレームワークであれば、これらの継承マッピングをサポートしているため、本当にエンティティ間の継承が必要な場合にインピーダンスミスマッチを和らげてくれます。
粒度のミスマッチ
オブジェクト指向設計を行う場合には、一般に大量のフィールドを持ったクラスを作成することは責務の観点から望ましくありませんが、データベースのテーブルを設計する場合には性能面も考慮して、1対1の関係にあるデータはあえて一つのテーブルにまとめてしまうことが望ましい場合が少なくありません。たとえば、顧客に対して送付先住所、課金先住所などの情報を別々のテーブルで持つ代わりに、一つのテーブルにまとめて保持させるということは実際よくやります。また、DDDのようなオブジェクト指向設計であれば、Stringやintといった基本的な値だけでなく、Date、Moneyなど特定の値と振る舞いをカプセル化した値オブジェクトを抽出して利用する場合があります。このように、テーブルとクラスとの間で望ましい粒度の違いがあるため、インピーダンスミスマッチの一つの原因となります。
この点についてもJPAでは@Embeddableを用いて値の埋め込みという手法である程度対処することができるようになっています。詳しくは以下を参照してください。
JPAの@Embeddableの使い道 - 達人プログラマーを目指して
なお、DDDでいうところのエンティティ、値オブジェクトとJPAの@Entity、@Embeddableとでは意味が微妙にずれているところがあり、単純な対応付けができない部分があります。この点については別の機会に私の考えをまとめてみたいと思います。
関連の方向性のミスマッチ
RDB上のテーブル間の関連は外部キーと主キーとのカラムとのマッピングで行うことが基本です。したがって、本来関連にはどちらのテーブルからどちらのテーブルという方向性という概念がありません。一方、クラス間の関連の場合、関連先のオブジェクトの参照をもう一方が保持するかどうかで双方向の関連にも一方向の関連にもなり得ます。そして、関連の方向性は依存関係に関与してくるため、オブジェクト指向の設計では欠かすことのできない重要な要素となります。
高度なO/Rマッピングのフレームワークでは、通常、一方向や双方向の関連を定義することができますが、方向性のないテーブルの関連にマッピングする際のミスマッチを吸収する工夫がされています。たとえば、Owner(飼い主)とDog(犬)というエンティティが一対多で関連づいていたとして
@Entity public Owner { @Id private Long id; @OneToMany(mappedBy = "owner") private List<Dog> dogs; ... } @Entity public Dog { @Id private Long id; @ManyToOne private Owner owner; ... }
のような双方向の関連を定義することができます。この場合、飼い主の犬との関連はクラス上は、飼い主→犬のリスト、犬→飼い主というお互いに独立した二つの方向の関連で定義されています。しかし、テーブル上はDOG表の外部キー列でOWNER表の主キーを参照することでマッピングします。もし、犬の所有者を変更するなどがあった場合に関連の紐付けを変更する必要があるのですが、その場合にオブジェクト上は2箇所の関連を書き換える必要がありますが、更新すべきテーブル上のカラムの値は一箇所です。オブジェクト上の変更を単純にテーブルの更新にマッピングしてしまうと、無意味に2回の更新をかけることになってしまうため、JPAでは@ManyToOne側の関連のみが更新に関与するという動作を行うことにより、この二重更新を防ぐ工夫がされています。*5
完全O/Rマッピングは動的なインピーダンスミスマッチも解決する
本格的なO/Rマッピングフレームワークではこのように継承、埋め込みオブジェクト、関連などのデータ構造の違いに起因する静的なインピーダンスミスマッチを吸収してくれますし、インピーダンスミスマッチといえばこのような静的なものを思い浮かべる人が実際多いと思います。実際、多くの内製フレームワークがサポートしている範囲はほとんどの場合せいぜいこの範囲のサポートに留まっています。しかし、JPAをサポートするような完全O/Rマッピングフレームワークでは、エンティティの状態更新とテーブルの更新頻度の違いというインピーダンスミスマッチの動的な側面にも光を当てています。
単純なCRUD処理が中心のアプリケーションであれば、データの更新はテーブルの行単位で実行されるため、特にこの動的なインピーダンスミスマッチが問題になることはありません。しかし、DDDが対象とするような本格的なドメインモデルにおいては、エンティティや集約そのものが状態をカプセル化する*6だけでなく、状態を部分的に更新したり、関連先の複数オブジェクトにまたがって更新するような複雑な振る舞いを保持すると考えます。このような場合には
- エンティティの状態が更新されたかどうかによらず行全体を毎回UPDATEするのは非効率
- エンティティの状態を部分的に更新するだけなので行全体のロックをトランザクションの最初から取得するのは無駄
- 複数のエンティティの更新順序によってデッドロックの危険もある
- 同一IDの複数のエンティティのインスタンスがメモリ中で存在した場合、更新漏れや上書きが起こるおそれがある
- 関連先のエンティティを必ず参照するとは限らないため毎回ロードしてしまうのは無駄
といったような問題が出てきます。つまり、更新頻度がオブジェクトとテーブルとで異なるということが本質的です。これは動的なインピーダンスミスマッチの問題と呼ばれていますが、RDBMS上で本格的なオブジェクト指向のドメインモデル設計を行う場合には無視のできない問題となります。これらの問題に対処するためには
- Identity Map → エンティティのキーに対するエンティティオブジェクトのキャッシュを保持し、作業単位(unit of work)*7内でのエンティティインスタンスの同一性を保障することで混乱を避ける。
- Unit of Work → 作業単位内でオブジェクトに起こった更新を記録し、必要に応じてコミット前などにまとめて更新する。*8
- Lazy Load → 関連先オブジェクトを一度に取得せず、必要に応じて後から取得する。
- Optimistic Offline Lock → バージョン番号チェックなどにより同時更新時の上書きを防ぐ
などのパターンが知られています。JPAでもこれらパターンが実装されています。実際、JPAのEntityManagerは伝統的なDAOと違って、UPDATEやINSERTなどのタイミングをアプリケーションプログラムが明示的に制御することは通常しません。普通にエンティティオブジェクトの状態を更新するとUPDATE文や行ロックの取得は裏で非同期に処理されます。そのため、動的なインピーダンスミスマッチの問題が軽減されます。
まとめ
O/Rマッピングといっても、テーブルの行を一つのクラスのインスタンスに単純に詰め替えるような単純なものから、JPAのように完全O/Rマッピングを実装したものまでさまざまなものがあります。O/Rマッピングの種類により、どのレベルまでインピーダンスミスマッチが吸収されるかについての違いがあります。
もちろん、インピーダンスミスマッチを吸収するためには複雑な仕組みが必要となるため、必ずしも常に高度なO/Rマッピングが望ましいということではなく、要件の性質に応じて適切な手段を使い分けることが必要です。しかし、少なくともDDDが目指すような本格的なオブジェクト指向設計のドメインモデルを実装に取り入れる際にはこれらのインピーダンスミスマッチに対処してくれるフレームワークをインフラストラクチャとして利用し、正しく使いこなすことが重要になってきます。
*1:Atomicity(原子性)、Consistency(一貫性)、Isolation(独立性)、Durability(永続性)
*2:未だに内製にこだわっている組織もあるかもしれませんが、少なくともO/Rマッピングのような汎用的なコンポーネントはOSSなどの広く使われている製品を流用するのが今では常識ではないでしょうか。
*3:今では、そのような単純なアプリケーションをJavaでわざわざカスタム開発すること自体が疑問視されるべきですが。
*5:mappedBy属性のある@OneToMany側をinverseサイド、外部キーの更新に関与する側をownerサイドと呼ぶ
*6:より望ましい設計としては一意性にかかわらない状態を値オブジェクトとして抽出して委譲させる
*7:多くの設定では作業単位はDBのトランザクションと一致するが、作業単位が複数のDBトランザクションを含むように構成することもできる。
*8:JPAでは作業単位(永続コンテキスト)内で状態の更新が記録されているエンティティのインスタンスをManagedエンティティと呼びます。