はじめに
アプリケーション・アーキテクチャについて議論する中で、最近は DDD の戦術的設計やクリーンアーキテクチャなどがキーワードとして解説されることが多いです。
そんな中、データアクセスの実装については Repository 意外の情報源が少なく、また Repository についても「データアクセス用のクラス」くらいの曖昧な定義で使われているケースも多いと思います。
この記事では、そんなデータアクセスのパターンについて改めて整理し、よく見かける議論の 1 つである「Rails の ActiveRecord や Laravel の Eloquant による Repository の実装」についても考察してみます。
注意事項
内容の正確性について
この記事はドメイン駆動設計に関する各種書籍や PoEAA などを参考にしていますが、情報源によって同じ言葉が違う意味で使われていることがあります。
できるだけよく使われている意味で整理しようとしていますが、筆者の主観が入っている部分もありますので、ご了承ください。
用語について
記事の中で
- ビジネスロジック
- データアクセス
- ドメインモデル
という用語を使いますが、こちらは過去に書いた
という記事内と同じ意味で使っています。
また、上記の記事の「コアなルール」のことを「ドメインロジック」と表現させていただきます。
表記について
記事の中で書籍『エンタープライズアプリケーションアーキテクチャパターン』を参考にしている箇所では『PoEAA』と省略表記させていただきます。
データアクセスのパターン整理
それでは本題に入ります。
この記事では、まず最初にデータアクセスについて以下の 3 つのパターンを整理してみます。
- Table Data Gateway パターン
- ActiveRecord パターン
- Repository パターン
細かく分類すればもっと多数のパターンに分けられますが、まずはこのくらいの分類が分かりやすいのではないかという考えです。
また、これらと併用して QueryService というクラスを設けるケースもありますが、そちらの説明は今回は省略します。 QueryService については以下の記事が非常に分かりやすいです。
では、ここから上記の 3 パターンについて書いていきます。
Table Data Gateway パターン
Table Data Gateway パターンをざっくり言えば、DB のテーブルと 1 対 1 に対応するデータアクセス用のクラスを設けるというものです。
Java 界隈には Data Access Object (DAO) と呼ばれるパターンがありますが、DAO は Table Data Gateway と同じものを指していると言われています。
実装イメージ
擬似言語による実装イメージは以下のようになります。
TaskTableDataGateway {
find(id)
findWithName(name)
insert(name)
update(id, name)
delete(id)
}
※ メソッド名は『PoEAA』p153 を参考にしています。
メリット
DB のテーブルと 1 対 1 に対応するクラスを作成してデータを取り出すのは、非常にシンプルで理解が簡単です。
デメリット
呼び出し側がデータモデルを意識することになります。 例えば Service から Table Data Gateway を呼び出す場合、ビジネスロジックがデータモデルに依存することになります。
また、Service クラスが Table Data Gateway を呼び出すようなアーキテクチャの場合、1 つの Service クラスが必要とする Table Data Gateway の数がかなり多くなるケースがあります。
さらに、Order と OrderDetail など、同時に DB からロードすべきデータを同時にロードすることを保証できず、ビジネスロジック層でオブジェクトの整合性がとれていない瞬間を生んでしまいます。
なお、これら 3 つのデメリットは、Repository パターンを使うことで解消可能です。
関連パターン
類似のパターンとして、以下のような実装も考えられます。
TaskTableDataGateway {
find(id)
findWithName(name)
insert(task)
update(task)
delete(task)
}
違いは insert、update、delete メソッドの引数がテーブルと対応するクラスのオブジェクトになっていることです。
ActiveRecord パターン
続いて、ActiveRecord パターンについてです。
ActiveRecord という単語で Ruby on Rails の OR マッパー「ActiveRecord」を思い浮かべる方も多いと思いますが、それは ActiveRecord パターンを実装したものです。
同様に、Laravel の Eloquant も ActiveRecord パターンを実装した OR マッパーです。
ActiveRecord パターンでは、ドメインモデルを DB のテーブルと 1 対 1 対応させ、データアクセスのメソッドも持たせます。 (ここで言うドメインモデルは、データとドメインロジックを持つクラスのことです)
実装イメージ
擬似言語による実装イメージは以下のようになります。
Task {
id
name
isAssigned()
insert()
update()
delete()
}
※ メソッド名は『PoEAA』p170 を参考にしています。
このように、
- id、name といったデータ
- isAssigned といったドメインロジックのメソッド
- insert や update、delete のようなデータアクセスのメソッド
を同じクラスが持つことになります。
※ メソッド名は OR マッパーにより異なる場合があります
メリット
ActiveRecord パターン最大のメリットは、Rails の ActiveRecord や Laravel の Eloquant といった ActiveRecord 系の OR マッパーを使うことで、非常に低コストで実装できることです。
また、これらのフレームワークは近年プログラミングの入門として選択されることも増えており、経験のあるエンジニアが多いです。
デメリット
ActiveRecord パターンでは、ドメインモデルと DB が 1 対 1 対応になるため、ビジネスロジックはデータモデルと強く結合します。
また、1 つのクラスがドメインモデルとデータアクセスという複数の役割を持つことになってしまいます。
Repository パターン
最後に Repository パターンです。
Repository パターンは、コレクションライクなインタフェースで、「集約」という単位でデータを読み書きするデータアクセスのパターンです。
今まで紹介した 2 パターンと異なり、Repository は DB と 1 対 1 対応させるのではなく、ドメインモデルの都合でデータアクセスを取り扱います。
実装イメージ
擬似言語による実装イメージは以下のようになります。
TaskRepository {
taskOfId(id)
taskOfName(name)
save(task)
remove(task)
}
※ メソッド名は『実戦ドメイン駆動設計』p405 を参考にしています。
これだけ見ると Table Data Gateway パターンと同じように見えるかもしれませんが、
- Table Data Gateway は DB のテーブルと 1 つと対応
- 1 つの Repository に対応する DB のテーブル数は状況次第
といった違いがあります。
この違いについては以下の記事が非常に分かりやすいです。
メリット
Repository を使うと、Repository を呼び出すビジネスロジック層が、データモデルに依存しなくなります。 そのため、ヘキサゴナルアーキテクチャ・オニオンアーキテクチャ・クリーンアーキテクチャなどで語られる、依存性逆転が可能になります。
また、Table Data Gateway とは異なり、Service などのクラスに多数必要とされにくくなったり、ロードしたオブジェクトの整合性のない状態を防ぐことができます。
デメリット
Repository パターンのメリットや目的を理解するのは、他のパターンと比べて何段階か難易度が高いです。
そのため、どんなパターンなのか理解しているエンジニアも少ないです。
関連パターン
上の例では最近 DDD などの文脈で見かけるリポジトリの実装イメージを示しましたが、『PoEAA』p345 では以下のようなインタフェースとして紹介されています。
TaskRepository {
matching(criteria)
}
検索条件を表す criteria オブジェクトを引数として検索するというものです。
どちらが正しいということはできませんが、重要なポイントは、どちらも「ドメインモデルを中心的に考えたインタフェースになっていること」だと思います。
どのパターンを使うか
ここまで、最近の開発でよく見かけるデータアクセスのパターンを整理してきました。
この中のどのパターンを使うかですが、「上記のメリット・デメリットを踏まえて、今回の状況ではどうするかを考える」というのが私の意見です。
例えば、「DDD だから Repository じゃないと絶対にダメ」といったことは必ずしもなく、トレードオフを理解した上で ActiveRecord パターンを採用してもいいと思います。
それ以上に前提として理解しておくべきポイントとして、どのパターンを使うかは、
- ビジネスロジック層とのインタフェース
- データアクセス層の内部実装
の 2 箇所について検討する必要があります。
ビジネスロジック層とのインタフェース
データアクセスの処理は主に Service (または ApplicationService、UseCase。アーキテクチャによっては Controller) といったクラスから呼び出すことになりますが、呼び出し先として
- Table Data Gateway
- ActiveRecord
- Repository
のどれを使うか、という議論がまずあります。
これは各パターンのメリット・デメリットを踏まえて決めることになります。
その内部実装についてが少しややこしいかもしれません。
データアクセス層の内部実装
ActiveRecord パターンを採用する場合は、ActiveRecord 系の OR マッパーを使うことが多いです。 しかし、Table Data Gateway や Repository を使う場合は、その内部を自前で実装することが多いです。
ここでは特に Repository の内部実装について書いていきます。
Repository の内部実装の方法
Repository の内部を実装する際は、何らかの OR マッパーを使うことが多いです。
OR マッパーとしては、
- SQL とマッピング方法の 2 つを書く方式
- 流れるようなインタフェース (fluent interface)
- JPA などの狭義の OR マッパー
- ActiveRecord 系の OR マッパー
などがあります。
また、Table Data Gateway を Repository の内部実装として使うといったこともできます。
Repository と他のデータアクセスパターンの関係
ここでポイントは、Repository の内部に Table Data Gateway や ActiveRecord が登場するケースがあることです。
つまり、Table Data Gateway や ActiveRecord には、
- Repository は使わず、ビジネスロジック層とのインタフェースを Table Data Gateway (または ActiveRecord) にする
- ビジネスロジック層とのインタフェースは Repository とし、内部実装を Table Data Gateway (または ActiveRecord) にする
といった選択肢があるということになります。
最後に、後者の「ビジネスロジック層とのインタフェースは Repository とし、内部実装を Table Data Gateway (または ActiveRecord) にする」パターンについて書いていきます。
Rails の ActiveRecord や Laravel の Eloquant による Repository の実装について
Rails の ActiveRecord や Laravel の Eloquant といった ActiveRecord 系の OR マッパーは広く使われています。 そのため、「Rails や Laravel にクリーンアーキテクチャを適用する」といった目的で、Repository の内部実装として ActiveRecord や Eloquant が使われている例を見かけることがあります。
これに対して、「Rails の ActiveRecord や Laravel の Eloquant を使うなら、基本的に Repository は設けないほうが良い」というのが今の私の考えです。
理由は主に 2 つあります。
理由 1
ActiveRecord パターンはデータベースと密結合になる代わりに実装コストを下げるのがメリットであり、Repository パターンとは逆の特徴を持ちます。
そのため、ActiveRecord と Repository を組み合わせると、ActiveRecord パターンのメリットは得られなくなります。
Repository パターンを採用する前提で、Repository の内部実装にすぎないと割り切って ActiveRecord 系の OR マッパーを使うことはありえなくはないですが、あまりメリットは大きくないと思います。
理由 2
ActiveRecord 系の OR マッパーが付属するフレームワークとして有名な Rails や Laravel は、ヘキサゴナルアーキテクチャ・オニオンアーキテクチャ・クリーンアーキテクチャといった、Repository パターンを活かせる設計と相性がよくないとも考えています。
ヘキサゴナルアーキテクチャ・オニオンアーキテクチャ・クリーンアーキテクチャといった設計は、クラス数が増える代わりに疎結合で変更に強くなるといった特徴があります。
これは Rails や Laravel などのスタンダードな実装とは対局にあり、言語としても、静的型付けのものを採用した方が相性が良いと思います。
落としどころ
以上の理由から、「Rails の ActiveRecord や Laravel の Eloquant を使うなら、基本的に Repository は設けないほうが良い」と考えています。
Rails や Laravel でアーキテクチャを改善するのであれば、
- Request クラスや Response クラスをしっかり定義する
- Model にしっかりメソッドを持たせ、適宜 ValueObject を作る
- ユースケースは UseCase (または Service、ApplicationService) クラスに分離する
といったところから着手するのが望ましいと思います。
例えば以下の書籍や記事が非常に参考になります。
- パーフェクト Ruby on Rails 【増補改訂版】 (Perfect series)
- 肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)|TechRacho by BPS株式会社
- Rails サービスクラス再考 / have a rethink on Rails service class - Speaker Deck
- 5年間 Laravel を使って辿り着いた,全然頑張らない「なんちゃってクリーンアーキテクチャ」という落としどころ
この内容と関連して、自分も過去に以下のような関連記事を書いています。
おわりに
以上、データアクセスのパターンについて考えていたことをまとめました。
他にも様々な実装方法があると思いますが、勉強中の方の理解のとっかかりになれば嬉しく思います。
用語の定義など含め、ご指摘事項などあれば Twitter の DM などでいただけますと幸いです。
参考書籍
- エンタープライズアプリケーションアーキテクチャパターン
- エリック・エヴァンスのドメイン駆動設計
- 実践ドメイン駆動設計
- .NETのエンタープライズアプリケーションアーキテクチャ 第2版
- Clean Architecture
- ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本
- 「実践ドメイン駆動設計」から学ぶDDDの実装入門
関連記事
以下、自分が過去に書いた関連記事です。