Laravel

【Laravel】子、孫を含めた3つのテーブルからリレーションデータを取得する方法(Eloquentリレーション、ネスト)

Laravelを使って、子、孫を含めた3つのテーブルからid(主キー)が一致するデータを取得(Eloquentリレーション、ネスト)し、一覧ページで表示する方法をご紹介します。

3つのテーブル構造

以下の3つのテーブルを用意しました。listpagesテーブルから商品を登録したユーザーが登録されてるusersテーブル(子)。usersテーブルに登録されてる都道府県id(todofuken_id)と一致するtodofukensテーブル(孫)の都道府県名(prefecture)を表示させます。

todofukensテーブル(孫)

名前 説明
id 主キー
prefecture 都道府県名

usersテーブル(子)

名前 説明
id 主キー
name ユーザー名
email メールアドレス
todofuken_id 都道府県ID=todofukensテーブルのid
※todofukensテーブルのテーブル名から「s」を除いた名前「todofuken」とtodofukensテーブルの主キー「id」にアンダーバー「_」を付けた名前「todofuken_id」にする必要があります。こうすることでtodofukensテーブルからidが一致したデータを取得することができます。

listpagesテーブル

名前 説明
id 主キー
product_name 商品名
user_id 商品登録ユーザーID=usersテーブルのid
※usersテーブルのテーブル名から「s」を除いた名前「user」とusersテーブルの主キー「id」にアンダーバー「_」を付けた名前「user_id」にする必要があります。こうすることでusersテーブルからidが一致したデータを取得することができます。

子、孫を含めた3つのテーブルからリレーションデータを取得するためのModelの記述

※ListpageモデルにUserモデル、Todofukenモデルのデータを取得する記述をします。

class Listpage extends Model
{
	public function user()
	{
		//Userモデルのデータを取得する
	    return $this->belongsTo('App\User');
	}
	public function todofukens()
	{
		//Todofukenモデルのデータを取得する
	    return $this->belongsTo('App\Todofuken');
	}
}

子、孫を含めた3つのテーブルからリレーションデータを取得するためのControllerの記述

※listpagesテーブルのuser_idと一致するusersテーブルからidとnameを取得します。usersテーブルのtodofuken_idと一致するtodofukensテーブルからidとprefectureを取得します。
 with([‘user:id,name’,’user.todofuken:id,prefecture’])を使うことでusersテーブルから取得したいカラムのデータを指定し、usersテーブルにネストしたtodofukensテーブルの取得したいカラム(id,prefecture)を指定して取得します。

use App\User;   //Userモデルを使用
use App\Listpage;   //Listpageモデルを使用


    public function listdata(Request $request)
    {
        $sort = $request->sort;
        $order = $request->order;

	//listpagesテーブルのuser_idと一致するusersテーブルからidとnameを取得します。
	$listpages = Listpage::with(['user:id,name','user.todofuken:id,prefecture'])->orderBy('id', 'asc')->paginate(20);

        return view('listpages', [
            'listpages' => $listpages,
        ]);
    }

子、孫を含めた3つのテーブルからリレーションデータを取得するためのデータを表示させるview(一覧ページ)の記述

※listpagesテーブルの孫テーブルとなるtodofukensテーブルの都道県を表示させる場合は「{{ $listpage->user->todofuken->prefecture }}」を記述します。

<table>
    <thead>
        <th>商品名</th>
        <th>登録ユーザー</th>
        <th>都道府県</th>
    </thead>
    <tbody>
        @foreach ($listpages as $listpage)
            <tr>
                <!-- 商品名 -->
                <td class="table-text">
                    <div>{{ $listpage->product_name }}</div>
                </td>
                <!-- 登録ユーザー -->
                <td class="table-text">
                    <div>{{ $listpage->user->name }}</div>
                </td>
                <!-- 都道府県 -->
                <td class="table-text">
                    <div>{{ $listpage->user->todofuken->prefecture }}</div>
                </td>
            </tr>
        @endforeach
    </tbody>
</table>

Routeの記述

※ページURLは必要に応じて変更して下さい。

Route::get('/listpages', 'ListpagesController@listdata');

 

【追記】laravel eloquent with の真価:パフォーマンスと「N+1問題」

上記の内容では、with(['user:id,name','user.todofuken:id,prefecture']) という形で laravel eloquent with を使い、ネストされたリレーションを賢く取得する方法が紹介されています。
しかし、なぜこの with() を使うことが重要なのでしょうか? それは「N+1問題」と呼ばれる、データベースクエリのパフォーマンスを著しく低下させる問題を解決するためです。

1. N+1問題:with を使わない場合の悲劇

もし、コントローラで laravel eloquent with を使わずに、単に以下のように記述したとします。

// 【悪い例】with() を使わずにデータを取得
$listpages = Listpage::orderBy('id', 'asc')->paginate(20);

そして、View側で元記事のように {{ $listpage->user->name }}{{ $listpage->user->todofuken->prefecture }} と記述してループ処理(@foreach)を行うと、Laravelは以下のような動きをします。

  1. (1回目) listpages テーブルから全件(20件)を取得するクエリが1回発行されます。
  2. (2回目) 1件目の $listpageuser を取得するために、users テーブルにクエリが発行されます。
  3. (3回目) 1件目の usertodofuken を取得するために、todofukens テーブルにクエリが発行されます。
  4. (4回目) 2件目の $listpageuser を取得するために、users テーブルにクエリが発行されます。
  5. (5回目) 2件目の usertodofuken を取得するために、todofukens テーブルにクエリが発行されます。
  6. …これが20件分繰り返されます。

結果として、20件のデータを表示するだけで「1 + (20 × 2) = 41回」ものクエリが発行されてしまいます。これが「N+1問題」(正確には 2N+1 問題)です。データ件数が増えれば、発行されるクエリ数も線形に増加し、ページの表示速度は壊滅的になります。

2. イーガーローディング:laravel eloquent with が行う「一括読み込み」

そこで laravel eloquent with の出番です。このメソッドは「イーガーローディング(Eager Loading)」を指示するもので、「関連するモデルをあらかじめ一括で読み込んでおく」ようLaravelに伝えます。

元記事の(カラム指定を省略した)with(['user', 'user.todofuken']) を使うと、クエリの実行は以下のようになります。

// 【良い例】laravel eloquent with を使用
$listpages = Listpage::with(['user', 'user.todofuken'])
                   ->orderBy('id', 'asc')
                   ->paginate(20);

この場合、発行されるクエリはたったの3回です。

  1. listpages テーブルから全件(20件)を取得するクエリ。 (SELECT * FROM listpages ...)
  2. 1.で取得した listpages 全件分の user_id を使い、users テーブルからまとめてユーザー情報を取得するクエリ。 (SELECT * FROM users WHERE id IN (1, 2, 5, ...))
  3. 2.で取得した users 全件分の todofuken_id を使い、todofukens テーブルからまとめて都道府県情報を取得するクエリ。 (SELECT * FROM todofukens WHERE id IN (1, 3, 13, ...))

表示するデータが20件でも1000件でも、発行されるクエリは3回で済みます。これが laravel eloquent with を使う最大の理由であり、実用的なLaravelアプリケーション開発において必須のテクニックです。

【超実用的】laravel eloquent with でカラム指定する際の「落とし穴」

元の記事では、さらに一歩進んで with(['user:id,name','user.todofuken:id,prefecture']) のように、読み込むカラムを限定しています。これは、不要なデータ(例:users テーブルの passwordremember_token)を読み込まないようにする、メモリ効率を上げるための非常に優れたテクニックです。

しかし、これには重大な「落とし穴」があります。

laravel eloquent with でカラムを指定する際、リレーションを機能させるために必要な「キー(主キーや外部キー)」を必ず含める必要があります。

元の記事のコードをもう一度見てみましょう。

// 元の記事のコード
$listpages = Listpage::with(['user:id,name','user.todofuken:id,prefecture'])
                  ->orderBy('id', 'asc')->paginate(20);

このコードは、実は user.todofuken の読み込みに失敗し、Viewで {{ $listpage->user->todofuken->prefecture }} を呼び出した際にエラーになる可能性が非常に高いです。

なぜなら、「user.todofuken」という孫リレーションを読み込むためには、その親である「user」モデルが「todofuken_id」カラムを持っている必要があるからです。
しかし、'user:id,name' の指定では todofuken_id を読み込んでいないため、Laravelは user から todofuken を見つけることができません。

laravel eloquent with を使ってネストされたリレーションのカラム指定を正しく行うには、以下のように中間(この場合は user)の外部キー(todofuken_id)も明示的に含める必要があります。

// 【正しいコード】中間テーブルの外部キー 'todofuken_id' を 'user' の指定に含める
$listpages = Listpage::with([
                       'user:id,name,todofuken_id', // <-- 'todofuken_id' を追加!
                       'user.todofuken:id,prefecture'
                   ])
                   ->orderBy('id', 'asc')
                   ->paginate(20);

このように、laravel eloquent with は非常に強力ですが、カラム指定を行う際はリレーションに必要なキーを省略しないよう、細心の注意を払いましょう。

 
※流用される場合は自己責任でお願いします。