リレーション先を検索する・リレーション先で検索する
..
Laravelを始めて以来戸惑っていたリレーションについて、最近までに勉強したノウハウをまとめてみます。
取り上げる例の設定
モデル | テーブル |
---|---|
User | users |
Follow | follows |
FollowRequest | follow_requests |
Avatar | avatars |
仕様:
- なんらかのSNS的なもの
- ユーザーがユーザーをフォローするには申請と承認が必要
-
follows
テーブルで紐づいているユーザーを「フォロワー」とする - 申請すると
FollowRequest
モデルのインスタンスが生成され、follow_requests
のfollower_id
(申請された側)とuser_id
(申請した側)に値が入る - 承認すると
承認ID
に値が入る+follows
テーブルで紐づけられる
一対多
hasManyメソッド
ユーザー1人に対し、申請先は多数存在することができます。
follow_requests
user_id_ | follower_id | 承認ID |
---|---|---|
申請した側 | 申請された側 | |
5 | 2 | 1 |
5 | 9 | NULL |
5 | 12 | 1 |
5 | 6 | NULL |
5 | 8 | NULL |
なので、ユーザー側から「フォロー申請を送ったユーザー」を取得してくるために両者の関係が「一対多」であることを定義することができます。
一対多の関係において、「一」側から「多」側のモデルを呼び出す際に用いるのがhasMany
メソッドです。
リレーションを使った絞り込み
とあるユーザーが行ったフォロー申請から「承認されていない申請」のみ取得してくるとします。
Eloquentで普通に書いた場合は以下のようになると思います。
$follow_requests = FollowRequest::where('user_id', $user->id)->whereNull('承認ID')->get();
一覧画面に表示したい場合など、明確にデータを取得してくる必要性があればこの実装でも問題ないのですが、
「申請が未承認だった場合は処理Aを行い、承認されていれば処理Bを行う」
といったような分岐を実装する際には、いちいちEloquentでテーブルを参照するのは冗長であり、またテーブルを参照できれば済むため->get()
を使う必要もありません。
こういったケースでリレーションを用いることで、モデルとモデルの関係性を定義し、よりすっきりしたコードで書くことができるようになります。
class User extends Model
{
・
・
// 申請情報を取得
public function requestsForUser () {
return $this->hasMany('App\Models\FollowRequest');
}
$user = User::find(5);
// 未承認の申請があるかどうかを参照する
$follow_requests = $user->requestsForUser() {
->whereNull('承認ID')
->exists();
}
if ($follow_requests) {
return '承認されていないフォロー申請がありますよ';
}
Readable.comには以下のような記載があります。
Eloquentは、親モデル名に基づきリレーションの外部キーを決定します。
今回の例であれば親モデルはUser
です。なので自動的にuser_id
というカラム名の値に絞って参照されるので、上記のように書いた場合は「user_id
が$user->id
であること」を指定する必要がなく、その他の条件を書くだけで絞り込みを行うことができます。
多対多
ユーザー同士はfollows
テーブルの中で紐づけられており、ユーザーとフォロワーの関係が定義されています。
follows
user_id | follower_id |
---|---|
フォローしている側 | フォローされている側 |
5 | 2 |
7 | 26 |
3 | 8 |
8 | 9 |
5 | 12 |
3 | 5 |
5 | 8 |
ユーザーはフォロワーでもあり、フォロワーはユーザーでもあるため、ユーザー同士は多対多の関係となっている状態です。
では、仮にページネーションを伴う「フォロワー一覧画面」を実装したい場合。さらにはフォロワーを検索で絞り込む機能も付けたい場合は、どのような実装にすればいいでしょうか。
belongsToManyメソッド
まずはUser
モデル内で、リレーションを定義します。多対多の場合はbelongsToMany
です。
class User extends Model
{
・
・
// フォロワーを取得
public function followers() {
return $this->belongsToMany('App\Models\User', 'follows', 'user_id', 'follower_id');
}
引数の説明をすると、「Userモデルのインスタンスを」「follows
テーブルの中の」「user_idがユーザーIDと等しいレコードの中の」「follower_idと等しいレコードに絞って」取得してくる、という指定になっています。
今回の設定では「フォロワー」の仕様上の定義を「follows
テーブルで紐づけられたユーザー」としているため、このような定義となります。
検索で絞り込む
こうした一覧画面の実装では、例えばquery
メソッドでこのように書くと思います。
$users = User::query();
// 入力に応じてwhere句での絞り込みを行う
if (isset($data['name'])) {
$name = $data['name'];
// 名前かカナで部分一致検索
$user->where(function($q) use($name)) {
$q->where('name', 'like', "{%$name%}")
->orWhere('kana', 'like', "{%$name%}");
});
}
$count = $users->count();
$userList = $users->paginate(20);
return [$userList, $count];
上記は、Userテーブルから全てのレコードを取得して、1ページごとに20人表示させる例です。検索欄に入力があれば、それを元にname
とkana
のカラムに対して部分一致検索をかけています。
これが多対多リレーション先の場合、このようになります。
$user = User::find($userId);
// この時点で$userのフォロワーのみに絞られる
$followers = $user->followers();
if (isset($data['name'])) {
$name = $data['name'];
$followers->where(function($q) use($name) {
$q->where('name', 'like', "%{$name}%")
->orWhere('kana', 'like', "%{$name}%");
});
}
$count = $followers->count();
$userList = $followers->paginate(20);
return [$followerList, $count];
Userモデル内で定義しているfollowers()
メソッドを叩くことによって、それ以後クエリをつなげていくことができます。
一対多の所属元で所属先を検索
例えば、いくつか種類のあるAvatarからUserが好きなものを選んで使用する場合は、Avatar1に対して複数のユーザーが存在しているため一対多の関係です。
では「使用しているアバターからユーザーを絞り込む」となった場合は、「多」側であるUserから「一」側であるAvatarを呼び出すことになります。その際のメソッドはbelongsTo
です。
belongsToメソッド
class User extends Model
{
・
・
// アバターを取得
public function avatar() {
return $this->belongsTo('App\Models\Avatar');
}
ユーザー一覧から「使用しているアバター名」で検索を行うとします。
このような「リレーション先の条件でリレーション元の絞り込みを行う」といった場合にはwhereHas
を使用し、クロージャで条件を付加します。
use App\Models\User;
use App\Models\Avatar;
・
・
・
$users = User::query();
// 入力されたアバター名で部分一致検索
if (isset($data['avatar_name'])) {
$keyword = $data['avatar_name'];
$users->whereHas('avatar', function($query) use($keyword){
$query->where(function($q) use($keyword) {
$q->where('name', 'like', "%{$keyword}%")
->orWhere('kana', 'like', "%{$keyword}%");
});
});
}
「リレーション先が存在しないこと」を条件としたい場合もあるかもしれません。
そのような場合にはwhereDoesntHave
を用いることで、リレーション先のこの条件に「当てはまらない」ものに絞り込むことができます。
// 入力されたアバターの名前、カナと一致しないものを検索
if (isset($data['avatar_name'])) {
$keyword = $data['avatar_name'];
$users->whereDoesntHave('avatar', function($query) use($keyword){
$query->where(function($q) use($keyword) {
$q->where('name', 'like', "%{$keyword}%")
->orWhere('kana', 'like', "%{$keyword}%");
});
});
}