【Laravel 設計 相談】Repository の捉え方について
背景
ドメイン駆動設計?クリーンアーキテクチャ?レイヤードアーキテクチャ?Repository パターン?
今自分が思考していることが何に当てはまるのかが分からない程度の理解。
設計に関する本はちょこちょこ読み進めているが、より具体的な部分での捉え方について、モヤモヤと思考していることが多く、識者の方々はどう捉えているのか大変興味があるので、可能な範囲で聞いてみたい。
そもそも僕の前提が間違えている部分があれば、ぜひご指摘をいただきたく、学びにしたい。
という考えです。
前提(自身の知識量紹介も込み)
- Laravelでの設計をベースに考えています。
- (知識ないなら大人しくEloquentに従えよ という言葉は一度飲み込んでいただけると🙇)
- クリーンアーキテクチャ的な設計で考えています。
- しかしエヴァンス本も読んだこともなければ、Clean Architecture の本もまだ読んだことないです。
- が、約1年ほどレイヤードアーキテクチャをベースに独自カスタマイズした設計の案件でRails書いてました。
- そこで興味を持ち、色々な方が投稿してくださるクリーンアーキテクチャに関する記事を読み漁って今に至る程度の知識量です。
- サンプルで記載するRepositoryのコードについて、戻り値がEntityではなく Eloquent Model なのは一旦無視してください。
- Serviceは、Usercase, Interactor のような、ビジネスロジック的な部分と捉えて書いています。
本編(自身が捉えるRepositoryについて)
Repositoryというものを、以下のように捉えています。
- 基本的には Model と Repository は対になる関係
- Repositoryからのレスポンスでは、原則として対となるModelの情報のみを返し、リレーションしている情報は返さない。
// /Service/User/GetService.php
$user = $user_repository->find(1);
// UserRepository
public function find($id): User
{
// 純粋にUserを返す。with 等を使って、リレーションする情報は返さない。
return User::find($id);
}
では、user_idを持つ Role というテーブルが存在して、Userに紐づくRoleをどう取得するのかというと、以下のイメージです。
// /Service/User/GetService.php
$user = $user_repository->find(1);
$roles = $role_repository->listByUserId($user->id);
// UserRepository
public function find(int $id): User
{
return User::find($id);
}
// RoleRepository
/**
* @return Collection<Role[]>
*/
public function listByUserId(int $id): Collection
{
return Role::whereId($id);
}
僕は今回挙げた例のように、欲しい情報は一括で取得せず、Serviceで順を追って必要なRepositoryにアクセスし、データを取得する方針が良いと考えています。
が、いくつか問題点があります。
問題点1
「DBへのリクエスト数が増える」という問題です。
ActiveRecord機能を使えば、UserRepositoryにアクセスし、リレーションを活用することで、一発で取得できます。
今回のような例だと、Eloquentを使って一括取得した方がスマートな気がします。
ただこの懸念点として、「複数のモデルの情報を一発で取得しようとしてクエリが複雑化する」という問題にぶつかります。
そのため個人的には、Serviceで順を追って処理を書いた方が好みで、以下のようなメリットを享受できると考えています。
- 何を目的としてRepositoryにアクセスをして、どのような順番でデータを取得したかが把握しやすくなる
- Serviceに色濃く依存したRepositoryメソッドが減り、汎用的に使いまわせるメソッドが増える
メリット1: 何を目的としてRepositoryにアクセスをして、どのような順番でデータを取得したかが把握しやすくなる
複雑なクエリだと、何を目的として絞り込みをかけ、何がレスポンスとして返ってくるのかが分かりづらいことがあります。
// Eloquent フル活用の例
/**
* 戻り値はCollectionで中身はUserの配列だが、リレーションしてる値も入ってるから純粋にUserとは言えない。
* @return Collection<User[]>
*/
public function listByXxx(...$何かしら引数): Collection
{
return User::join()
->join()
->join()
->where()
->where()
->where()
->where();
}
UserRepositoryからは、必ず「Userの情報 or データ型系」しか返ってこないという縛りが生まれる事で、理解がしやすいコードになると個人的には考えています。
メリット2: Serviceに色濃く依存したRepositoryメソッドが減り、汎用的に使いまわせるメソッドが増える
フェイズを分けて取得することで一つ一つのRepositoryメソッドがスリムになる可能性が高いです。
そのため、ある特定のケースに依存せず、他のケースでもそのメソッドを使用できます。
// UserReopsitory
/**
* @return Collection<User[]>
*/
public function listById(int $id): Collection
{
return User::where($id);
}
// HogeRepository
/**
* @return Collection<Hoge[]>
*/
public function listByXxx(int $id): Collection
{
return Hoge::where($id);
}
// FugaRepository
/**
* @return Collection<Fuga[]>
*/
public function listByXxx(int $id): Collection
{
return Fuga::where($id);
}
自身の考え
今回の例のようなレベルであれば Eloquent を活用した一括取得でも良いと思いますが、リレーションを使うケースと使わないケースが混在するとチーム開発において実装方針にブレが生じるので、どちらかに統一させた実装にしたいと考えています。
そのため僕としては、DBへのリクエスト数が増える問題を理解した上で、複雑なケースにも対応できる「Serviceで順を追って必要なRepositoryにアクセスする」という手段を推しています。
問題点2
「中間テーブルを挟む場合はどうするの?」という問題です。
リレーションテーブルに自身のIDを持ってくれる場合は問題ないのですが、一つテーブルが挟む場合、この手段が通じなくなります。
選択肢としては2つあります。
- 中間テーブルに対して、Repository を設ける。
- 例外的に
whereHas
のみ、リレーション関連を使用することを認める。
選択肢1: 中間テーブルに対して、Repository を設ける
自身の方針を貫ける、モデルとリポジトリが基本的に対になる状態にできます。
しかし、中間テーブルに対してもRepositoryを作成する場合、使い道が "中間テーブルとしての繋ぎ" しかないことが気になります。
おそらく作成するメソッドも、IDをリターンするか、存在するかどうかのBooleanを返すかどうかで、中間テーブル単体のモデルを返すことは基本的に起きないと考えています。
そのため、中間テーブルに対してRepositoryを設けるのは少しやりすぎかも?なんてことを思います。
whereHas
のみ、リレーション関連を使用することを認める
選択肢2: 例外的に 中間テーブルを通じてリレーションするテーブルのIDを取得し、そのIDを持ってリレーションのリポジトリにデータと取りにいくという方法です。
中間テーブルが存在する場合は例外的に、 whereHas
を使ってデータを取得することは許容することで、「レスポンスの内容は必ずそのRepositoryと対になるモデルのみ」というルールを守ることができます。
自身の考え
僕はこの場合であれば選択肢2を推したいと考えています。
基本的にはルールを守りたいですが、中間テーブルのためのRepositoryはあまりにも用途が絞られる気がしました。
例外的に許すものを明確にすることで、秩序はそれなりに守られるんじゃないかと考えています。
まとめ
どちらにも共通して言えるのが、「Repositoryで他モデルの関心事は持つの?」という点です。
自分の中の結論としては、
- 基本的には Model と Repository は対になる関係
- Repositoryからのレスポンスでは、原則として対となるModelの情報のみを返し、リレーションしている情報は返さない。
- 中間テーブルを挟む場合は、例外的に
whereHas
を認めて対応する
になったのですが、記事によっては Eloquent をもっと活用すべき だったり、自分が所属する会社ではガンガンJoinして他のテーブルの登録処理までやっちゃって と、無秩序に感じることが多々あります。
ただ、自身の考えが果たして正しいのか?良い考えなのか?という点については不安が大きいため、ぜひいろんな方の考えや、取り入れている設計方針について伺えると嬉しいと思った次第です。
この記事に書いていることが答えに近いかも。
基本的には Model と Repository は対になる関係だと思ったけど、違うかも。
RDBを前提に考えたら自然とModelが対になりそうな気はするけど、大切なのはモデリング。
例えば、User と UserDetail が存在したとき、これはまとめて User というモデルで捉えるべき。
UserDetail単体で存在することはなく、User とセットで捉えるべき情報。
なので、他モデルの情報が含まれること自体を問題視する必要は無いかもしれない。
ただやっぱり、User と Role のように、UserRoletテーブルといった中間テーブルを挟む必要がある状況の場合はどのようにモデリングすべきか?というのは悩みどころ。
Userが持つ属性としてRoleがある場合は、Userを主として Role も含めたモデルとして捉えて扱うことが適切かな?
ただ見方を変えると、Role単位でユーザーが所属しているみたいな場合は Roleを主として User を含めたモデルとして捉えて扱うこともありそう。
これはどちらが主なのか?という考えは不要で、「User + Role を合わせた一つのモデル」という扱いで、どちらが主でも関係ない?とも見れそう。
モデリング難しい問題。
あと、ミノ駆動さんのこのポストはぶっ刺さった。
完全にWeb脳。