テーブルBと、B.idにリレーションを張ってるテーブルAがあったとします。
モデルは普通。
// モデルA
class TableA extends Model{
// テーブル名
protected $table = 'table_a';
/**
* リレーション
* @return BelongsTo
*/
public function tableb()
{
return $this->belongsTo(TableB::class, 'b_id', 'id');
}
}
// モデルB
class TableB extends Model{
// テーブル名
protected $table = 'table_b';
}
コントローラとテンプレも普通。
$tableas = TableA->get();
@foreach ($tableas as $tablea)
{{ $tablea->name }}
{{ $tablea->tableb['name'] }}
@endforeach
さくっとできました。
できたはいいけどn+1問題が直撃する書き方です。
ループのたびにSELECT * FROM table_b WHERE id=x
というSQLが走るので、とてもよろしくありません。
よってEagerローディングするようにしましょう。
コントローラにwith書くだけなので一瞬です。
$tableas = TableA->with('table_b')->get();
何十個も発行されていたSQLがわずか二つに減りました。
しかし、これもまだ無駄があります。
テーブルAもBもnameしか使ってないので、SELECT *
ではなくSELECT name
のSQLを発行するようにしたいです。
このためにLaravelにはスコープという機能があります。
// モデルA
class TableA extends Model{
// テーブル名
protected $table = 'table_a';
/**
* リレーション
* @return BelongsTo
*/
public function tableb()
{
return $this->belongsTo(TableB::class, 'b_id', 'id');
}
/**
* 取得対象カラム
*/
public static function scopeName($query){
return $query->select(['name']);
}
}
// モデルB
class TableB extends Model{
// テーブル名
protected $table = 'table_b';
/**
* 取得対象カラム
*/
public static function scopeName($query){
return $query->select(['name']);
}
}
コントローラからはスコープの呼び出しを追加します。
$tableas = TableA::name()->with(['tableb' => function ($q) {$q->name();}])->get();
やったね。
はい、これテーブルBのデータ取って来れません。
リレーション先テーブルのデータに主キーがなかった場合、何故か紐付けてくれないのです。
実はEagerローディングは予想に反してJOINしていません。
発行されたSQLを見ると、SELECT name FROM table_a WHERE id IN (1, 2, 3); SELECT name FROM table_b WHERE id IN (4, 5, 6);
みたいになっています。
別々に取ってきたデータを後でくっつけているので、くっつけるために主キーが必要になっています。
マニュアルにも特定カラムのEagerロードという項目で一応触れられているのですが、スコープとはだいぶ離れたところにあるうえにwith('author:id,name')
という例示のせいで一見同じものに見えないため、関係ないと読み流してしまいました。
ということできちんと結合するには、主キーも取ってくるようにしましょう。
// モデルB
public static function scopeName($query){
return $query->select(['id', 'name']);
}
まとめ
SELECT先を指定するときは、必ず主キーも入れよう。