リレーション先を検索する・リレーション先で検索する

..

Laravelを始めて以来戸惑っていたリレーションについて、最近までに勉強したノウハウをまとめてみます。

取り上げる例の設定

モデル テーブル
User users
Follow follows
FollowRequest follow_requests
Avatar avatars

仕様:

  • なんらかのSNS的なもの
  • ユーザーがユーザーをフォローするには申請と承認が必要
  • followsテーブルで紐づいているユーザーを「フォロワー」とする
  • 申請するとFollowRequestモデルのインスタンスが生成され、follow_requestsfollower_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人表示させる例です。検索欄に入力があれば、それを元にnamekanaのカラムに対して部分一致検索をかけています。

これが多対多リレーション先の場合、このようになります。

$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}%");
        });
    });
}