STEP1:メソッドのオプションに対応する
Repository層で必ず確認しなければいけないのは、利用するメソッドのオプションです。
フレームワークでは便利に使えるようにオプションが用意されています。
しかし、Repository層でメソッドを作成する際に、そのオプションの引数を対応していない方が多いです。
そうなると、元々は多様な状況に対応できたのに、限定的にしか使えなくなります。
そうならないためにも使用するメソッドの引数を必ず確認しましょう。
例をみてみよう。
find()の悪い例
よくある例として、id情報からfind()でレコードを取得するケースです。
よくあるのは、リレーションに対応していなかったり、特定カラムだけを取得するオプション忘れです。
例えば下記になります。
/**
* 特定のレコードを取得
*
* @param integer $id
* @return User
*/
public function find(int $id): User
{
return User::findOrFail($id);
}
一見問題ないように見えます。
確かにレコードの取得だけなら可能です。
しかし、それしかできません。
例えば下記の要件に当たると、別のメソッドを作成しなければいけません。
- リレーションを取得して、N+1問題に対応したい
- 取得するカラムを制限したい
これらの要件を満たすには、別途メソッド用意しなければなりません。
/**
* 特定のレコードを取得
*
* @param integer $id
* @return User
*/
public function find(int $id): User
{
return User::findOrFail($id);
}
/**
* 対象ユーザーとリレーションで紐づく製品情報も一括取得
*
* @param integer $id
* @return User
*/
public function findWithProducts(int $id): User
{
return User::with('products')->findOrFail($id);
}
上記のように、同じようなメソッドを量産していく必要が出てきます。
これは非常に無駄です。
各モデルごとにメソッドを作っていられませんよね。
そこでオプションを用意して、どんな状況にも対応できるメソッドを作成します。
レゴのブロックのように、どこにでも噛み合うようなメソッドが理想です。
メソッドのオプションを確認する
それでは利用するメソッドのオプションを確認していきましょう。
Laravelのフレームワークで定義されているfindOrFail()
は下記になります。
/**
* Find a model by its primary key or throw an exception.
*
* @param mixed $id
* @param array $columns
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|static|static[]
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findOrFail($id, $columns = ['*'])
{
$result = $this->find($id, $columns);
$id = $id instanceof Arrayable ? $id->toArray() : $id;
if (is_array($id)) {
if (count($result) === count(array_unique($id))) {
return $result;
}
} elseif (! is_null($result)) {
return $result;
}
throw (new ModelNotFoundException)->setModel(
get_class($this->model), $id
);
}
ここでわかる通り、2つの引数があります。
public function findOrFail($id, $columns = ['*'])
$id
は必須ですが、特定のカラムを取得するオプションとして$columns
が用意されています。
つまりRepository層でメソッドを利用する際は、引数に$id
と$columns
を定義しなければいけません。
次にリレーションも取得できるようにwith()
メソッドを確認します。
/**
* Set the relationships that should be eager loaded.
*
* @param string|array $relations
* @param string|\Closure|null $callback
* @return $this
*/
public function with($relations, $callback = null)
{
if ($callback instanceof Closure) {
$eagerLoad = $this->parseWithRelations([$relations => $callback]);
} else {
$eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations);
}
$this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad);
return $this;
}
with()では2つの引数、$relations
と$callback
が必要とわかります。
public function with($relations, $callback = null)
第二引数の$callback
はClosureということもあり、絞り込み条件などを書いたりできます。
Office::with(['users' => function($query) {
$query->orderBy('teamorder');
}])->orderBy('order')->get();
ここまで整理すると、4つの引数が必要だとわかりました。
$id
$columns
$relations
$callback
この4つの引数を使って、メソッド本来の良さを保ちつつ、Repository層でメソッドを作成します。
find()の良い例
メソッドの引数を確認したところで、どの要件にも対応できるメソッドを作成しましょう。
/**
* 特定のレコードを取得
*
* @param integer $id
* @param array|null $columns 指定して取得したいカラム
* @param array|null $with
* @param Closure|null $callback
* @return User
*/
public function find(int $id, ?array $columns = ['*'], ?array $with = [], ?Closure $callback = null): User
{
return User::with($with, $callback)->findOrFail($id, $columns);
}
必須の引数をまず定義し、オプションは最後に書いていきます。
これはメソッドのルールなので気をつけていきましょう。
実際に利用するときは、名前付き引数を利用すると、わかりやすくなります。
$this->userRepository->find(
$userId,
columns: ['id', 'name', 'email'],
with: ['product'],
);
これでメソッドのオプションに関しては以上です。
このように必ず元のメソッドのオプションを確認して、利便性を下げないように気をつけましょう。
STEP2: 集約の概念を取り入れる
集約は,部分のオブジェクトが集まって全体のオブジェクトを構成している関係を指す.全体のオブジェクトがなくなっても,部分はそれ自体で存在することができる.
http://teacher.nagano-nct.ac.jp/fujita/LightNEasy.php?page=Aggregation
集約を簡単に説明すると、関連するオブジェクトのセットだと思ってください。
集約の例
例えば、記事投稿する機能があるとします。
記事を保存するには、下記の3つの保存処理が必要です。
- 記事(Article)の保存
- カテゴリーやタグ(Category・Tag)の保存
- 画像(Image)の保存
このことから記事における集約は、記事
とカテゴリー(タグ)
、画像
の3点になります。
つまりRepository層で記事を保存する際、カテゴリー
と画像
も一緒に保存しなければいけません。
/**
* 記事の保存処理
*/
public function store()
{
DB::transaction(function () {
// 画像を保存
// カテゴリーを保存
// 記事を保存
});
}
コードの詳細は省くが、上記のように3つの保存処理をArticleRepository::store()
で定義する必要がある。
このように集約を利用することで、記事
・カテゴリー
・画像
の関係性がはっきりし、
仕様が理解しやすくなります。
なぜRepository層で集約を利用するのか?
「Service層で記事と画像、カテゴリーを保存してもよいではないか?」
と疑問に思う人もいるはずです。
それも一つの方法ですが、Repository層に書くことで、書き漏れを防げます。
例えば、Service層で複数の保存処理(条件が異なるなど)があった場合、それぞれの保存処理で毎回カテゴリーと画像を保存する処理を書かないといけません。複数箇所で書く以上、書き漏れが生まれたりします。それを避けられるのがメリットです。
つまり、Repository層で書いている以上、他のサービス層では記述する必要がありません。
また集約することで、CategoryRepository
やImageRepository
など他のRepositoryファイルをわざわざ作成する必要はありません。
ArticleRepository
一つで完結するので、無駄なファイルを作成しなくても良くなります。つまり手間が減ります。
サービス層で定義しても実装は可能ですが、CategoryRepository
やImageRepository
は必要なため、作成する手間が生まれます。
この差が重要です。
要点を整理すると
- 集約を意識すると、一つのRepositoryで完結できる。無駄なRepositoryファイルを作成しなくても良い。
- Repository層で定義したので、毎回Service層でそれぞれの同じ処理を書かなくてもよい。書き漏れを防げる。
- Entityの関係性(仕様)が理解できる。
この3つのメリットがあります。
これらの理由から、Repository層で集約を取り入れるべきという話でした。
まとめ
最後にまとめに入ります
- Repository層はレゴのパーツのように、さまざまな状況に対応できるメソッドを作成しなければならない。
そのために、元のメソッドの引数は必ず確認して、オプションもしっかり定義する。 - 集約を意識して、関連するオブジェクトを同時に保存や更新し、Entityの関係性をわかりやすくする。
またこれにより、無駄なRepositoryファイルを生成しなくてもよい。
コメント