RitoLabo

LaravelのEloquentORMでモデルベースのDBリレーション~基本からEagerロードまで~

  • 公開:
  • カテゴリ: PHP Laravel
  • タグ: PHP,Laravel,5.5,5.4,5.3,EloquentORM,Model,5.6,Relationships,Polymorphic

Laravelで提供されているEloquentは、モデルを定義しデータベースの操作を行うO/Rマッパーです。

今回は、Eloquentでのリレーションの定義や考え方を見ていきます。

アジェンダ
  1. 開発環境
  2. リレーション
  3. hasOne() / 1対1
  4. belongsTo() / 1対1
  5. hasMany() / 1対多
  6. belongsTo() / 多対1
  7. belongsToMany() / 多対多
    1. 中間テーブルのカラム取得
      1. タイムスタンプカラム
      2. フィルタリング
  8. hasManyThrough()
  9. morphMany() / ポリモーフィック
  10. morphToMany() / 多対多ポリモーフィック
  11. morphedByMany()
  12. リレーションのクエリ
    1. has()
    2. whereHas() / orWhereHas()
    3. doesntHave() / orDoesntHave()
    4. whereDoesntHave() / orWhereDoesntHave()
    5. withCount()
  13. Eagerロード

開発環境

今回の開発環境は以下の通りです。

  • Linux CentOS 7
  • Apache 2.4
  • MySQL 5.7
  • PHP7.2/7.1
  • Laravel

Laravelのバージョンに関しては、5.6/5.5/5.4/5.3にて動作確認済みです。

尚、モデルを格納するディレクトが通常と違い app/Models 配下になっています。

それを含め、EloquentORMの基本に関しては以下を参考にしてください。
LaravelのEloquentORMの基本から具体的な使い方

リレーション

データベースには1つのスキーマに複数のテーブルがあり、その中にレコードが格納されていきます。

基本的にテーブルを分けるのは格納するデータの性質(ここでいう性質とは、例えばこっちのテーブルは野球チームのデータの集合、あっちのテーブルはサッカーチームのデータの集合。の様な事)が違うからですが、同じ性質を持ったテーブルを敢えて分ける事で、情報を整理整頓し、効率的にデータベースを使用する事が出来ます。

その時にはテーブル同士を繋げてさせて1つの情報(データ)を形成するのですが、それを「リレーション(結合)する」と言います。

Eloquentでは、その結合に関するパターン(テーブル同士の関係)を定義し詳細を設定する事でリレーションを行っていきます。

hasOne() / 1対1

1対1は、結合元テーブルのリレーションさせたい値に対して、結合先テーブルの対応する値が必ず1つのみである関係を言います。

例えば何らかの組織のメンバーを格納しているテーブルと、そのメンバーに貸与される電話番号テーブルがあるとします。

各メンバーが持つ電話番号は1つであり、複数貸与される事はありません。

つまり、メンバーテーブルのレコード1つが持つ電話番号情報は、電話番号テーブルに格納されているレコードのどれか1つだけに必ず合致する。

これが「1対1」の関係です。

これをEloquentのモデルで設定すると以下のようになります。

laravel/app/Models/Members.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Members extends Model
{
public function phone()
{
return $this->hasOne('App\Models\Phone');
}
}

1対1を定義する場合はhasOne()メソッドを使い、引数に結合対象のモデルを指定します。

また、上記の場合は暗黙的にidを外部キーmembers_idとリレーションさせる想定で動作します。つまり、membersテーブルのidとphonesテーブルのmembers_idをリレーションさせるという事です。

これを任意のカラムに変更するには、以下のように記述します。

return $this->hasOne('App\Models\Phone', '$foreign_key', '$local_key');

第二引数に外部キー(foreignKey)を指定し、第三引数にローカルキー(localKey)を指定します。これでmembersテーブルのlocal_keyとphonesテーブルのforeign_keyをリレーションします。

モデルの設定を行うとデータ取得の際にリレーションが行われ結合先のデータも取得できます。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Members;

class SampleController extends Controller
{
protected $members;

public function __construct()
{
$this->members = new Members();
}

public function index()
{
$data = [];
$members = $this->members->all();
$i = 0;
foreach ($members as $m) {
// membersテーブル - nameカラム
$data[$i]['name'] = $m->name;
// phonesテーブル - numberカラム
$data[$i]['phone'] = $m->phone->number;
$i++;
}
}
}

ポイントとしては、結合先テーブルにアクセスする際はテーブル名ではなくモデルで定義したメソッド名で行う点です。

$m->phone->number; // メソッド名でアクセス

public function phone() // モデル定義したメソッド

リレーションしたのはphonesテーブルですが、アクセスする際はモデルで定義したメソッド名のphoneです。

取得したデータは以下のようになります。

Array
(
[0] => Array
(
[name] => test01
[phone] => 09012345601
)

[1] => Array
(
[name] => test02
[phone] => 09012345602
)

[2] => Array
(
[name] => test03
[phone] => 09012345603
)

[3] => Array
(
[name] => test04
[phone] => 09012345604
)

[4] => Array
(
[name] => test05
[phone] => 09012345605
)

[5] => Array
(
[name] => test06
[phone] => 09012345606
)

[6] => Array
(
[name] => test07
[phone] => 09012345607
)

[7] => Array
(
[name] => test08
[phone] => 09012345608
)

[8] => Array
(
[name] => test09
[phone] => 09012345609
)

[9] => Array
(
[name] => test10
[phone] => 09012345610
)

)

メンバーのレコード1件につき、電話番号情報を1件取得できている事が確認できます。

belongsTo() / 1対1

1対1、つまり hasOne() でリレーションした両テーブルの関係は、「主たるテーブルと、それを補うテーブル」という事になりますが、この関係で逆からの取得を行いたい場合もあります。

今回の例で言えば、phonesテーブルからmembersテーブルの情報を取得する場合です。この場合には、Phone側のモデルを belongsTo() で定義します。

laravel/app/Models/Phone.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Phone extends Model
{
public function members()
{
return $this->belongsTo('App\Models\Members');
}
}

書式は同一で、belongsTo()メソッドにモデルの名前空間を引数に渡します。そしてこの場合は、暗黙的にphonesテーブルのmembers_idとmembersテーブルのidを結合させます。

外部キーやオーナーキーを任意のカラムに変更したい場合は以下のように記述します。

return $this->belongsTo('App\Models\Members', 'foreign_key_name', 'own_key_name');

それではコントローラからデータ取得を行います。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Phone;

class SampleController extends Controller
{
protected $phone;

public function __construct()
{
$this->phone = new Phone();
}

public function index()
{
$data = [];
$phones = $this->phone->all();
$i = 0;
foreach ($phones as $p) {
// phonesテーブル - numberカラム
$data[$i]['phone'] = $p->number;
// membersテーブル - nameカラム
$data[$i]['name'] = $p->members->name;
$i++;
}
}
}

結果は以下になります。

Array
(
[0] => Array
(
[phone] => 09012345601
[name] => test01
)

[1] => Array
(
[phone] => 09012345602
[name] => test02
)

[2] => Array
(
[phone] => 09012345603
[name] => test03
)

[3] => Array
(
[phone] => 09012345604
[name] => test04
)

[4] => Array
(
[phone] => 09012345605
[name] => test05
)

[5] => Array
(
[phone] => 09012345606
[name] => test06
)

[6] => Array
(
[phone] => 09012345607
[name] => test07
)

[7] => Array
(
[phone] => 09012345608
[name] => test08
)

[8] => Array
(
[phone] => 09012345609
[name] => test09
)

[9] => Array
(
[phone] => 09012345610
[name] => test10
)

)

補完側であるphonesテーブルから主体であるmembersテーブルの値を取得出来ている事が確認できました。

hasMany() / 1対多

membersテーブルとcommentsテーブルがあるとします。コメントテーブルには、メンバーのコメントが時系列に格納されています。

これを結合関係にした場合、メンバーは複数のコメントを持っている事になります。つまり、membersテーブルのレコードに紐づくcommentsテーブルのレコードが複数ある。という事になります。

この関係を「1対多」であるといいます。

この関係性をもったテーブルをリレーションするにはhasMany()としてモデルに定義します。

laravel/app/Models/Members.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Members extends Model
{
public function comments()
{
return $this->hasMany('App\Models\Comments');
}
}

この場合も、暗黙的にmembersテーブルのidとcommentsテーブルのmembers_idを結合します。

外部キーとローカルキーを変更する場合は以下のように記述します。

return $this->hasMany('App\Models\Comments', 'foreign_key', 'local_key');

第二引数に外部キーを、第三引数にローカルキーを渡します。これで、membersテーブルのlocal_keyカラムと、commentsテーブルのforeign_keyカラムによってリレーションが行われるようになります。

それではコントローラからデータを取得します。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Members;

class SampleController extends Controller
{
protected $members;

public function __construct()
{
$this->members = new Members();
}

public function index()
{
$members = $this->members->all();
$data = [];
$i = 0;
foreach ($members as $m) {
// membersテーブル
$data[$i]['name'] = $m->name;
foreach ($m->comments as $c) {
// membersテーブル
$data[$i]['comments'][] = $c->comment;
}
$i++;
}
}
}

結果は以下の通りです。

Array
(
[0] => Array
(
[name] => test01
[comments] => Array
(
[0] => Comment 01
[1] => Comment 03
[2] => Comment 08
[3] => Comment 10
)

)

[1] => Array
(
[name] => test02
[comments] => Array
(
[0] => Comment 06
)

)

[2] => Array
(
[name] => test03
[comments] => Array
(
[0] => Comment 09
)

)

[3] => Array
(
[name] => test04
[comments] => Array
(
[0] => Comment 02
)

)

[4] => Array
(
[name] => test05
)

[5] => Array
(
[name] => test06
)

[6] => Array
(
[name] => test07
[comments] => Array
(
[0] => Comment 04
[1] => Comment 05
)

)

[7] => Array
(
[name] => test08
)

[8] => Array
(
[name] => test09
[comments] => Array
(
[0] => Comment 07
)

)

[9] => Array
(
[name] => test10
)

)

1件のユーザレコードに対して複数件のコメントレコードを持つデータを取得できている事が確認できます。

belongsTo() / 多対1

逆の、commentsテーブルからmembersテーブルの情報を取得したい場合は、モデルを belongTo() メソッドで定義します。

laravel/app/Models/Comments.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comments extends Model
{
public function members()
{
return $this->belongsTo('App\Models\Members');
}
}

これは「多対1」とも呼ばれます。

コントローラからデータを取得します。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Comments;

class SampleController extends Controller
{
protected $comments;

public function __construct()
{
$this->comments = new Comments();
}

public function index()
{
$comments = $this->comments->all();
$data = [];
$i = 0;
foreach ($comments as $c) {
// commentsテーブル
$data[$i]['comment'] = $c->comment;
// membersテーブル
$data[$i]['name'] = $c->members->name;
$i++;
}
}
}

結果は以下になります。

Array
(
[0] => Array
(
[comment] => Comment 01
[name] => test01
)

[1] => Array
(
[comment] => Comment 02
[name] => test04
)

[2] => Array
(
[comment] => Comment 03
[name] => test01
)

[3] => Array
(
[comment] => Comment 04
[name] => test07
)

[4] => Array
(
[comment] => Comment 05
[name] => test07
)

[5] => Array
(
[comment] => Comment 06
[name] => test02
)

[6] => Array
(
[comment] => Comment 07
[name] => test09
)

[7] => Array
(
[comment] => Comment 08
[name] => test01
)

[8] => Array
(
[comment] => Comment 09
[name] => test03
)

[9] => Array
(
[comment] => Comment 10
[name] => test01
)

)

belongsToMany() / 多対多

リレーションとは基本的に複数のテーブルの結合関係を表しますが、その互いのテーブルにおいて、それぞれが、それぞれ複数の対象を持つ場合の結合関係を「多対多」といいます。

例えば、ブログ記事とそれに設定するタグで考えてみます。

記事はそれぞれいくつでも好きなだけ関連するタグを設定でき、タグ自身も、複数の記事に設定されます。

この場合、記事テーブルのタグIDカラムには複数のタグIDを入れる必要があり、同じくタグテーブルにも、1つのタグレコードの記事IDカラムには複数の記事IDを入れなければなりません。が、、データベースを簡潔に使おうと思ったらそれは難しい要件となります。

そこで、「中間テーブル」を用意する事で、この多対多の関係を構築する事が出来ます。

中間テーブルはその名の通り、対象のテーブルとテーブルの中間を保持するテーブルの事で、この例の場合ではブログ記事IDとタグIDを保持するレコードを持つ事で、両者をリレーションする。という仕組みになります。

各テーブルのカラム構成を見ると理解しやすいです。

// 記事テーブル
mysql
> show columns from posts;
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| post | text | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+------------------+------+-----+---------+----------------+

// 中間テーブル
mysql
> show columns from posts_tags;
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
|
posts_id | int(11) | NO | | NULL | |
|
tags_id | int(11) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+------------------+------+-----+---------+----------------+

// タグテーブル
mysql
> show columns from tags;
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| tag | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+------------------+------+-----+---------+----------------+

中間テーブルでは記事IDとタグIDを持ち、記事テーブルとタグテーブルを結びつける役割を担っている事がわかります。

中間テーブルを持つ事で「多対多」を表現でき、そしてこの「多対多」の関係性を築く事で、双方のレコードがそれぞれ複数の対象を持っていてもリレーションが可能になります。

この「多対多」のリレーションを行うには、モデルでbelongsToMany()メソッドを定義します。

laravel/app/Models/Posts.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Posts extends Model
{
public function tags()
{
return $this->belongsToMany('App\Models\Tags');
}
}

上記は最も最小の記述になりますが、この場合は以下が暗黙のルールとして処理が行われます。

  1. postsテーブルとtagsテーブルのリレーションである
  2. 中間テーブルにposts_tagsテーブルを使う
  3. postsテーブルのidとposts_tagsテーブルのposts_idが結合する
  4. tagsテーブルのidとposts_tagsテーブルのtags_idが結合する

よって、テーブル名や外部キーなどがこの暗黙ルールに沿っていない場合は、以下のように記述することで任意のカラム名などに変更できます。

return $this->belongsToMany(
'App\Models\Tags',
'posts_tags', // 中間テーブル名
'posts_id', // 中間テーブルにあるFK
'tags_id' // リレーション先モデルのFK
);

コントローラからデータを取得します。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Posts;

class SampleController extends Controller
{
protected $posts;

public function __construct()
{
$this->posts = new Posts();
}

public function index()
{
$posts = $this->posts->all();
$data = [];
$i = 0;
foreach ($posts as $p) {
// postsテーブル
$data[$i]['post'] = $p->post;
foreach ($p->tags as $t) {
// tagsテーブル
$data[$i]['tags'][] = $t->tag;
}
$i++;
}
}
}

結果は以下になります。

Array
(
[0] => Array
(
[post] => post01
[tags] => Array
(
[0] => tag03
[1] => tag07
[2] => tag05
[3] => tag01
[4] => tag02
)

)

[1] => Array
(
[post] => post02
)

[2] => Array
(
[post] => post03
[tags] => Array
(
[0] => tag03
)

)

[3] => Array
(
[post] => post04
[tags] => Array
(
[0] => tag01
)

)

[4] => Array
(
[post] => post05
)

[5] => Array
(
[post] => post06
[tags] => Array
(
[0] => tag09
)

)

[6] => Array
(
[post] => post07
)

[7] => Array
(
[post] => post08
[tags] => Array
(
[0] => tag02
)

)

[8] => Array
(
[post] => post09
[tags] => Array
(
[0] => tag03
)

)

[9] => Array
(
[post] => post10
)

)

記事ベースで関連する複数のタグを取得できている事が確認できました。

ちなみにこの逆、タグテーブルから記事を取得する場合も、同じくbelongsToMany()メソッドでモデルを定義してやれば取得が可能です。

laravel/app/Models/Tags.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Tags extends Model
{
public function posts()
{
return $this->belongsToMany('App\Models\Posts');
}
}
laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Tags;

class SampleController extends Controller
{
protected $tags;

public function __construct()
{
$this->tags = new Tags();
}

public function index()
{
$tags = $this->tags->all();
$data = [];
$i = 0;
foreach ($tags as $t) {
// tagsテーブル
$data[$i]['tag'] = $t->tag;
foreach ($t->posts as $p) {
// postsテーブル
$data[$i]['posts'][] = $p->post;
}
$i++;
}
}
}

結果は以下になります。

Array
(
[0] => Array
(
[tag] => tag01
[posts] => Array
(
[0] => post04
[1] => post01
)

)

[1] => Array
(
[tag] => tag02
[posts] => Array
(
[0] => post08
[1] => post01
)

)

[2] => Array
(
[tag] => tag03
[posts] => Array
(
[0] => post01
[1] => post03
[2] => post09
)

)

[3] => Array
(
[tag] => tag04
)

[4] => Array
(
[tag] => tag05
[posts] => Array
(
[0] => post01
)

)

[5] => Array
(
[tag] => tag06
)

[6] => Array
(
[tag] => tag07
[posts] => Array
(
[0] => post01
)

)

[7] => Array
(
[tag] => tag08
)

[8] => Array
(
[tag] => tag09
[posts] => Array
(
[0] => post06
)

)

[9] => Array
(
[tag] => tag10
)

)

タグベースで設定されている記事を取得出来ている事が確認できます。

中間テーブルのカラム取得

中間テーブルのカラムを取得する場合は、モデルにアクセスしたいカラムを指定し、取得時にpivotプロパティにアクセスします。

laravel/app/Models/Posts.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Posts extends Model
{
public function tags()
{
return $this->belongsToMany('App\Models\Tags')->withPivot('created_at', 'updated_at');
}
}
laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Posts;

class SampleController extends Controller
{
protected $posts;

public function __construct()
{
$this->posts = new Posts();
}

public function index()
{
$posts = $this->posts->find(1);
$data['post'] = $posts->post;
$i = 0;
foreach ($posts->tags as $t) {
$data[$i]['tag'] = $t->tag;
$data[$i]['created'] = $t->pivot->created_at;
$data[$i]['updated'] = $t->pivot->updated_at;

$i++;
}
}
}

1つの記事データを取得していますが、結果は以下になります。

Array
(
[post] => post01
[0] => Array
(
[tag] => tag03
[created] => Illuminate\Support\Carbon Object
(
[date] => 2018-06-17 09:10:41.000000
[timezone_type] => 3
[timezone] => UTC
)

[updated] => Illuminate\Support\Carbon Object
(
[date] => 2018-06-20 09:10:41.000000
[timezone_type] => 3
[timezone] => UTC
)

)

[1] => Array
(
[tag] => tag07
[created] => Illuminate\Support\Carbon Object
(
[date] => 2018-06-17 09:10:41.000000
[timezone_type] => 3
[timezone] => UTC
)

[updated] => Illuminate\Support\Carbon Object
(
[date] => 2018-06-20 09:10:41.000000
[timezone_type] => 3
[timezone] => UTC
)

)

[2] => Array
(
[tag] => tag05
[created] => Illuminate\Support\Carbon Object
(
[date] => 2018-06-17 09:10:41.000000
[timezone_type] => 3
[timezone] => UTC
)

[updated] => Illuminate\Support\Carbon Object
(
[date] => 2018-06-20 09:10:41.000000
[timezone_type] => 3
[timezone] => UTC
)

)

[3] => Array
(
[tag] => tag01
[created] => Illuminate\Support\Carbon Object
(
[date] => 2018-06-17 09:10:41.000000
[timezone_type] => 3
[timezone] => UTC
)

[updated] => Illuminate\Support\Carbon Object
(
[date] => 2018-06-20 09:10:41.000000
[timezone_type] => 3
[timezone] => UTC
)

)

[4] => Array
(
[tag] => tag02
[created] => Illuminate\Support\Carbon Object
(
[date] => 2018-06-17 09:10:41.000000
[timezone_type] => 3
[timezone] => UTC
)

[updated] => Illuminate\Support\Carbon Object
(
[date] => 2018-06-20 09:10:41.000000
[timezone_type] => 3
[timezone] => UTC
)

)

)

中間テーブルに格納されている作成日と更新日を取得できている事が確認できます。タイムスタンプに関しては、自動的にCarbonオブジェクトとして取得されている事も確認できます。

タイムスタンプカラム

ちなみにcreated_atとupdated_atカラム限定ですが、これらを取得したい場合はモデルにwithTimestamps()を指定すると有効になります。

return $this->belongsToMany('App\Models\Tags')->withTimestamps();

また、データ取得時にpivotプロパティへアクセスしますが、プロパティ名の変更もできます。

return $this->belongsToMany('App\Models\Tags')
->as('cng_name')
->withTimestamps();

モデルでas()メソッドをチェーンさせ、変更したいプロパティ名をセットする事で変更できます。

$data[$i]['created'] = $t->cng_name->created_at;
$data[$i]['updated'] = $t->cng_name->updated_at;

フィルタリング

多対多のリレーションを行う際に、中間テーブルを通して取得するデータをフィルタリングする事が出来ます。

// posts_id  3 のタグ情報を取得する
return $this->belongsToMany('App\Models\Tags')
->wherePivot('posts_id', 3)
->withTimestamps();

// posts_id 1 3 のタグ情報を取得する
return $this->belongsToMany('App\Models\Tags')
->wherePivotIn('posts_id', [1, 3])
->withTimestamps();

例として前者の、wherePivot()指定でのデータ取得を行います。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Posts;

class SampleController extends Controller
{
protected $posts;

public function __construct()
{
$this->posts = new Posts();
}

public function index()
{
$posts = $this->posts->all();
$data = [];
$i = 0;
foreach ($posts as $p) {
// postsテーブル
$data[$i]['post'] = $p->post;
$ii = 0;
foreach ($p->tags as $t) {
// tagsテーブル
$data[$i]['tags'][$ii]['tag'] = $t->tag;
// 中間テーブル
$data[$i]['tags'][$ii]['created'] = $t->pivot->created_at;
$data[$i]['tags'][$ii]['updated'] = $t->pivot->updated_at;
$ii++;
}
$i++;
}
}
}

結果は以下になります。

Array
(
[0] => Array
(
[post] => post01
)

[1] => Array
(
[post] => post02
)

[2] => Array
(
[post] => post03
[tags] => Array
(
[0] => Array
(
[tag] => tag03
[created] => Illuminate\Support\Carbon Object
(
[date] => 2018-06-17 09:10:41.000000
[timezone_type] => 3
[timezone] => UTC
)

[updated] => Illuminate\Support\Carbon Object
(
[date] => 2018-06-20 09:10:41.000000
[timezone_type] => 3
[timezone] => UTC
)

)

)

)

[3] => Array
(
[post] => post04
)

[4] => Array
(
[post] => post05
)

[5] => Array
(
[post] => post06
)

[6] => Array
(
[post] => post07
)

[7] => Array
(
[post] => post08
)

[8] => Array
(
[post] => post09
)

[9] => Array
(
[post] => post10
)

)

post_idが3のタグ情報のみが取得されている事が確認できます。

hasManyThrough()

LaravelのEloquentでは、テーブルをまたぐリレーションも行えます。

「テーブルをまたぐリレーション」とは、簡単に言うと3つのテーブルを使ったリレーションです。

例えば以下のような関係を持つテーブルがあるとします。

   members
↓↑ ↓↑
roles posts

メンバーレコードはそれぞれ、権限と記事を関連付けてデータを取得できる状況です。

モデルは以下のように定義されています。

laravel/app/Models/Members.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Members extends Model
{
public function posts()
{
return $this->hasMany('App\Models\Posts');
}

public function roles()
{
return $this->belongsTo('App\Models\Roles');
}
}
laravel/app/Models/Roles.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Roles extends Model
{
public function members()
{
return $this->hasMany('App\Models\Members');
}
}

コントローラでのデータ取得は以下のようになります。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Members;

class SampleController extends Controller
{
protected $members;

public function __construct()
{
$this->members = new Members();
}

public function index()
{
$data = [];
$members = $this->members->all();
$i = 0;
foreach ($members as $m) {
// membersテーブル
$data[$i]['name'] = $m->name;
// rolesテーブル
$data[$i]['role'] = $m->roles->name;
foreach ($m->posts as $p) {
// postsテーブル
$data[$i]['post'][] = $p->post;
}
$i++;
}
}
}

取得結果は以下のようになります。

Array
(
[0] => Array
(
[name] => test01
[role] => システム管理者
[post] => Array
(
[0] => post01
[1] => post04
[2] => post07
[3] => post10
)

)

[1] => Array
(
[name] => test02
[role] => 管理者
[post] => Array
(
[0] => post05
[1] => post06
)

)

[2] => Array
(
[name] => test03
[role] => システム管理者
[post] => Array
(
[0] => post03
)

)

[3] => Array
(
[name] => test04
[role] => 一般
[post] => Array
(
[0] => post09
)

)

[4] => Array
(
[name] => test05
[role] => 一般
)

[5] => Array
(
[name] => test06
[role] => 管理者
[post] => Array
(
[0] => post02
)

)

[6] => Array
(
[name] => test07
[role] => 一般
)

[7] => Array
(
[name] => test08
[role] => 管理者
)

[8] => Array
(
[name] => test09
[role] => 管理者
[post] => Array
(
[0] => post08
)

)

[9] => Array
(
[name] => test10
[role] => システム管理者
)

)

つまりはメンバーテーブルを軸に、権限・記事それぞれの該当情報を取得しているわけですが、この良くあるパターンの関係から、違う方向の関係をで取得したい場合の手法になります。

具体的にはrolesからpostsを取得する。rolesテーブルからmembersテーブルを経由してpostsテーブルを取得する。という事になります。

   members

roles posts

モデルで hasManyThrough() を定義します。

laravel/app/Models/Roles.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Roles extends Model
{
public function posts()
{
return $this->hasManyThrough('App\Models\Posts', 'App\Models\Members');
}
}

第一引数にはリレーションを行うモデルを、第二引数には経由するモデルを指定します。

上記設定の場合では以下を暗黙的なルールとして処理を行います。

  • リレーション先テーブル「posts
  • 経由テーブル「members
  • rolesテーブルのidとmembersテーブルのroles_idを結合
  • membersテーブルのidとPostsテーブルのmembers_idを結合

これを任意のものにしたい場合は以下のようにモデルを定義します。

return $this->hasManyThrough(
'App\Models\Posts',
'App\Models\Members',
'first_key', // membersテーブルの外部キー
'second_key', // postsテーブルの外部キー
'local_key', // rolesテーブルのローカルキー
'second_local_key' // membersテーブルのローカルキー
);

コントローラからデータを取得します。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Roles;

class SampleController extends Controller
{
protected $roles;

public function __construct()
{
$this->roles = new Roles();
}

public function index()
{
$data = [];
$roles = $this->roles->all();
$i = 0;
foreach ($roles as $r) {
// rolesテーブル
$data[$i]['role'] = $r->name;
foreach ($r->posts as $p) {
// postsテーブル
$data[$i]['post'][] = $p->post;
}
$i++;
}
}
}

結果データは以下になります。

Array
(
[0] => Array
(
[role] => システム管理者
[post] => Array
(
[0] => post01
[1] => post03
[2] => post04
[3] => post07
[4] => post10
)

)

[1] => Array
(
[role] => 管理者
[post] => Array
(
[0] => post02
[1] => post05
[2] => post06
[3] => post08
)

)

[2] => Array
(
[role] => 一般
[post] => Array
(
[0] => post09
)

)

)

その権限を持つユーザが持つ記事を取得出来ている事が確認できました。

morphMany() / ポリモーフィック

ポリモーフィック(多様性=Polymorphic)リレーションは、1つのテーブルで複数の関連するテーブルの管理を行う結合関係です。例えば、以下のような構造になります。

posts  feed
↓↑ ↓↑
comments

記事とつぶやき集合のようなフィードにコメントを付けられるとして、そのコメントを一つのテーブルで管理する。こんな関係です。

テーブルは以下のようになります。

mysql> show columns from posts;
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| members_id | text | NO | | NULL | |
| post | text | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+------------------+------+-----+---------+----------------+

mysql> show columns from feeds;
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| body | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+------------------+------+-----+---------+----------------+

mysql> show columns from comments;
+------------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| comment | text | NO | | NULL | |
| commentable_id | int(11) | NO | | NULL | |
| commentable_type | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------------+------------------+------+-----+---------+----------------+

まずはモデルにポリモーフィックを定義します。主たるモデルにmorphMany()で定義します。

laravel/app/Models/Posts.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Posts extends Model
{
public function comments()
{
return $this->morphMany('App\Models\Comments', 'commentable');
}
}
laravel/app/Models/Feeds.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Feeds extends Model
{
public function comments()
{
return $this->morphMany('App\Models\Comments', 'commentable');
}
}

2つのモデルでmorphMany()メソッドに同じ引数を渡しています。第一引数にはコメントを保持するモデル、第二引数にはidとtypeを紐づけるPrefix(プレフィックス)を設定しています。

つまり、上記でcommentableと設定した事で、記事とフィードのidをcommentable_idカラムと結び付け、記事なのかフィードなのかを区別する為にcommentable_typeカラムを見る。という事になります。

これを任意のもので設定したい場合は以下のように記述します。

return $this->morphMany(
'App\Models\Comments',
'name',
'type',
'id',
'local_key'
);
  • 第一引数 - 結合先モデル
  • 第二引数 - プレフィックス
  • 第三引数 - typeとしての識別カラム
  • 第四引数 - 外部キー
  • 第五引数 - ローカルキー

具体的に、今回の例で記述すると以下になります。

return $this->morphMany(
'App\Models\Comments',
null,
'commentable_type',
'commentable_id',
'id'
);

第二引数に関しては、第三・四引数で個別に指定しているので未指定でOKです。

それではコントローラからデータ取得を行います。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Posts;

class SampleController extends Controller
{
protected $posts;

public function __construct()
{
$this->posts = new Posts();
}

public function index()
{
$posts = $this->posts->all();
$data = [];
$i = 0;
foreach ($posts as $p) {
// postsテーブル
$data[$i]['id'] = $p->id;
$data[$i]['post'] = $p->post;
foreach ($p->comments as $c) {
// commentsテーブル
$data[$i]['comments'][] = $c->comment;
}
$i++;
}
}
}

結果は以下になります。

Array
(
[0] => Array
(
[id] => 1
[post] => post01
[comments] => Array
(
[0] => post Comment 01
[1] => post Comment 02
[2] => post Comment 06
)

)

[1] => Array
(
[id] => 2
[post] => post02
[comments] => Array
(
[0] => post Comment 04
)

)

[2] => Array
(
[id] => 3
[post] => post03
[comments] => Array
(
[0] => post Comment 05
)

)

[3] => Array
(
[id] => 4
[post] => post04
)

[4] => Array
(
[id] => 5
[post] => post05
)

[5] => Array
(
[id] => 6
[post] => post06
)

[6] => Array
(
[id] => 7
[post] => post07
[comments] => Array
(
[0] => post Comment 03
)

)

[7] => Array
(
[id] => 8
[post] => post08
)

[8] => Array
(
[id] => 9
[post] => post09
)

[9] => Array
(
[id] => 10
[post] => post10
)

)

記事に対するコメントが取得出来ている事が確認できます。Feedの方も取得してみます。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Feeds;

class SampleController extends Controller
{
protected $feeds;

public function __construct()
{
$this->feeds = new Feeds();
}

public function index()
{
$feeds = $this->feeds->all();
$data = [];
$i = 0;
foreach ($feeds as $f) {
// feedsテーブル
$data[$i]['id'] = $f->id;
$data[$i]['feed'] = $f->body;
foreach ($f->comments as $c) {
// commentsテーブル
$data[$i]['comments'][] = $c->comment;
}
$i++;
}
}
}

結果は以下になります。

Array
(
[0] => Array
(
[id] => 1
[feed] => feed body01
[comments] => Array
(
[0] => feed Comment 04
)

)

[1] => Array
(
[id] => 2
[feed] => feed body02
)

[2] => Array
(
[id] => 3
[feed] => feed body03
)

[3] => Array
(
[id] => 4
[feed] => feed body04
[comments] => Array
(
[0] => feed Comment 01
)

)

[4] => Array
(
[id] => 5
[feed] => feed body05
)

[5] => Array
(
[id] => 6
[feed] => feed body06
)

[6] => Array
(
[id] => 7
[feed] => feed body07
[comments] => Array
(
[0] => feed Comment 02
)

)

[7] => Array
(
[id] => 8
[feed] => feed body08
)

[8] => Array
(
[id] => 9
[feed] => feed body09
[comments] => Array
(
[0] => feed Comment 03
)

)

[9] => Array
(
[id] => 10
[feed] => feed body10
)

)

フィードに対するコメントが取得できている事が確認できます。

ここで1つポイントです。

モデルの定義やコントローラからの取得は難しくありませんが、ここで大事なのはどういった形でこれらのデータを格納するかです。

今回の例でいうと、postsやfeedsテーブルのレコードはidによって結合されるのでいつも通りですが、ポリモーフィックを判別するcommentsテーブルのtypeカラムはモデル名を格納する必要があります。


、、が、実際にはモデル名を格納するのではなく、名前空間+モデル名の文字列を格納します。つまりはこうです。

mysql> select * from comments;
+----+-----------------+----------------+------------------+------------+------------+
| id | comment | commentable_id | commentable_type | created_at | updated_at |
+----+-----------------+----------------+------------------+------------+------------+
| 1 | post Comment 01 | 1 | App\Models\Posts | NULL | NULL |
| 2 | feed Comment 01 | 4 | App\Models\Feeds | NULL | NULL |
| 3 | post Comment 02 | 1 | App\Models\Posts | NULL | NULL |
| 4 | post Comment 03 | 7 | App\Models\Posts | NULL | NULL |
| 5 | feed Comment 02 | 7 | App\Models\Feeds | NULL | NULL |
| 6 | post Comment 04 | 2 | App\Models\Posts | NULL | NULL |
| 7 | feed Comment 03 | 9 | App\Models\Feeds | NULL | NULL |
| 8 | feed Comment 04 | 1 | App\Models\Feeds | NULL | NULL |
| 9 | post Comment 05 | 3 | App\Models\Posts | NULL | NULL |
| 10 | post Comment 06 | 1 | App\Models\Posts | NULL | NULL |
+----+-----------------+----------------+------------------+------------+------------+

名前空間まで付与した形の文字列を格納しないと、判別が行えず、狙ったデータを取得する事ができないので注意が必要です。

morphToMany() / 多対多ポリモーフィック

ポリモーフィックリレーションにさらに多対多の関係性を加えた結合モデルです。

例えば、記事とフィードのテーブルが、共通のタグ情報を持つテーブルとそれぞれ結合する場合などです。モデルの関係は以下のようになります。

posts feeds
↓↑ ↓↑
taggables
↓↑
tags

これらのテーブルの構造は以下になります。

mysql> show columns from posts;
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| members_id | text | NO | | NULL | |
| post | text | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+------------------+------+-----+---------+----------------+

mysql> show columns from feeds;
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| body | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+------------------+------+-----+---------+----------------+

mysql> show columns from tags;
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| tag | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+------------------+------+-----+---------+----------------+

mysql> show columns from taggables;
+---------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+-------+
| tags_id | int(11) | NO | | NULL | |
| taggable_id | int(11) | NO | | NULL | |
| taggable_type | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+---------------+--------------+------+-----+---------+-------+

多対多ポリモーフィック結合を設定するには、モデルへmorphToMany()メソッドで定義します。

laravel/app/Models/Posts.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Posts extends Model
{
public function tags()
{
return $this->morphToMany('App\Models\Tags', 'taggable');
}
}
laravel/app/Models/Feeds.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Feeds extends Model
{
public function tags()
{
return $this->morphToMany('App\Models\Tags', 'taggable');
}
}

2つのモデル共、共通な設定になりますが、第一引数に結合対象であるモデルを、第二引数にはidとtypeのプレフィックスを渡します。

全てを任意の値で設定する場合は以下のように記述します。

return $this->morphToMany(
'App\Models\Tags',
'type_prefix',
'table_name',
'foreign_pivot_key',
'related_pivot_key',
'parent_key',
'related_key',
false
);
  • 第一引数 - 結合先モデル
  • 第二引数 - 中間テーブルtypeカラムのプレフィックス
  • 第三引数 - 中間テーブル名
  • 第四引数 - 結合元モデルと結合させる中間テーブルのカラム
  • 第五引数 - 結合先モデルと結合させる中間テーブルのカラム
  • 第六引数 - 結合元モデルの結合カラム
  • 第七引数 - 結合先モデルの結合カラム
  • 第八引数 - リレーションの逆を接続しているか。設定はbool値。デフォルトはfalse

今回の例では以下のようになります。

return $this->morphToMany(
'App\Models\Tags',
'taggable',
'taggables',
'taggable_id',
'tags_id',
'id',
'id',
false
);

それではコントローラからデータを取得します。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Posts;

class SampleController extends Controller
{
protected $posts;

public function __construct()
{
$this->posts = new Posts();
}

public function index()
{
$posts = $this->posts->all();
$data = [];
$i = 0;
foreach ($posts as $p) {
// postsテーブル
$data[$i]['id'] = $p->id;
$data[$i]['post'] = $p->post;
foreach ($p->tags as $t) {
// tagsテーブル
$data[$i]['tags'][] = $t->tag;
}
$i++;
}
}
}

結果は以下になります。

Array
(
[0] => Array
(
[id] => 1
[post] => post01
[tags] => Array
(
[0] => tag01
[1] => tag03
[2] => tag01
)

)

[1] => Array
(
[id] => 2
[post] => post02
[tags] => Array
(
[0] => tag07
)

)

[2] => Array
(
[id] => 3
[post] => post03
[tags] => Array
(
[0] => tag01
)

)

[3] => Array
(
[id] => 4
[post] => post04
)

[4] => Array
(
[id] => 5
[post] => post05
)

[5] => Array
(
[id] => 6
[post] => post06
)

[6] => Array
(
[id] => 7
[post] => post07
)

[7] => Array
(
[id] => 8
[post] => post08
)

[8] => Array
(
[id] => 9
[post] => post09
)

[9] => Array
(
[id] => 10
[post] => post10
)

)

記事に対するタグ情報を取得出来ている事が確認できます。フィードの方も取得します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Feeds;

class SampleController extends Controller
{
protected $feeds;

public function __construct()
{
$this->feeds = new Feeds();
}

public function index()
{
$feeds = $this->feeds->all();
$data = [];
$i = 0;
foreach ($feeds as $f) {
// feedsテーブル
$data[$i]['id'] = $f->id;
$data[$i]['feed'] = $f->body;
foreach ($f->tags as $t) {
// tagsテーブル
$data[$i]['tags'][] = $t->tag;
}
$i++;
}
}
}

結果は以下になります。

Array
(
[0] => Array
(
[id] => 1
[feed] => feed body01
[tags] => Array
(
[0] => tag02
[1] => tag02
)

)

[1] => Array
(
[id] => 2
[feed] => feed body02
)

[2] => Array
(
[id] => 3
[feed] => feed body03
)

[3] => Array
(
[id] => 4
[feed] => feed body04
[tags] => Array
(
[0] => tag04
)

)

[4] => Array
(
[id] => 5
[feed] => feed body05
)

[5] => Array
(
[id] => 6
[feed] => feed body06
[tags] => Array
(
[0] => tag09
)

)

[6] => Array
(
[id] => 7
[feed] => feed body07
)

[7] => Array
(
[id] => 8
[feed] => feed body08
[tags] => Array
(
[0] => tag05
)

)

[8] => Array
(
[id] => 9
[feed] => feed body09
)

[9] => Array
(
[id] => 10
[feed] => feed body10
)

)

フィードに対するタグ情報を取得出来ている事が確認できました。

morphedByMany()

多対多ポリモーフィックの逆からの取得、つまりはTagモデルを主軸として、そのタグに含まれている記事やフィードを取得する事も出来ます。この場合は、モデルへmorphedByMany()メソッドで定義します。

laravel/app/Models/Tags.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Tags extends Model
{
public function posts()
{
return $this->morphedByMany('App\Models\Posts', 'taggable');
}

public function feeds()
{
return $this->morphedByMany('App\Models\Feeds', 'taggable');
}
}

コントローラから取得します。

laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Tags;

class SampleController extends Controller
{
protected $tags;

public function __construct()
{
$this->tags = new Tags();
}

public function index()
{
$tags = $this->tags->all();
$data = [];
$i = 0;
foreach ($tags as $t) {
// tagsテーブル
$data[$i]['tag'] = $t->tag;
foreach ($t->posts as $p) {
// postsテーブル
$data[$i]['posts'][] = $p->post;
}
foreach ($t->feeds as $f) {
// feedsテーブル
$data[$i]['feeds'][] = $f->body;
}
$i++;
}
}
}

結果は以下になります。

Array
(
[0] => Array
(
[tag] => tag01
[posts] => Array
(
[0] => post01
[1] => post03
[2] => post01
)

[feeds] => Array
(
[0] => feed body01
)

)

[1] => Array
(
[tag] => tag02
[feeds] => Array
(
[0] => feed body01
)

)

[2] => Array
(
[tag] => tag03
[posts] => Array
(
[0] => post01
)

)

[3] => Array
(
[tag] => tag04
[feeds] => Array
(
[0] => feed body04
)

)

[4] => Array
(
[tag] => tag05
[feeds] => Array
(
[0] => feed body08
)

)

[5] => Array
(
[tag] => tag06
)

[6] => Array
(
[tag] => tag07
[posts] => Array
(
[0] => post02
)

)

[7] => Array
(
[tag] => tag08
)

[8] => Array
(
[tag] => tag09
[feeds] => Array
(
[0] => feed body06
)

)

[9] => Array
(
[tag] => tag10
)

)

タグを主としてそれに属する記事とフィードを取得できている事が確認できました。

リレーションのクエリ

Eloquentでは、リレーションしたモデルに対して制約を付けてデータを取得できます。

has()

has()メソッドを使うと、リレーション先の値を持つ主データを取得できます。

// タグを持つ記事のみを取得する
$posts = $this->posts->has('tags')->get();

件数などを細かく指定し取得も行えます。第二引数に演算子、第三引数に指定数を渡しています。

// タグを2つ以上持つ記事のみを取得する
$posts = $this->posts->has('tags', '>=', 2)->get();

whereHas() / orWhereHas()

複雑な制約を付けたい場合はwhereHas()メソッドを使い、クロージャを併用して条件を定義できます。

// 文字列「tag」を含むタグ名を持つ記事のみを取得する
$posts = $this->posts->whereHas('tags', function ($query) {
$query->where('tag', 'like', '%tag%');
})->get();

doesntHave() / orDoesntHave()

制約には「有する」だけでなく「持っていない」場合のクエリも用意されています。その時はdoesntHave()メソッドを使います。

// タグを持たない記事のみを取得する
$posts = $this->posts->doesntHave('tags')->get();

whereDoesntHave() / orWhereDoesntHave()

has()と同様より複雑な条件式を定義するなら、whereDoesntHave()を用います。第二引数にクロージャを渡し、where句を付与できます。

// 文字列「AAA」を含むタグ名を持たない記事のみを取得する
$posts = $this->posts->whereDoesntHave('tags', function ($query) {
$query->where('tag', 'like', '%AAA%');
})->get();

withCount()

結合先モデルの件数もwithCount()メソッドを用いる事で取得できます。

// タグの件数も取得する
$posts = $this->posts->withCount('tags')->get();

$data = [];
$i = 0;
foreach ($posts as $p) {
// postsテーブル
$data[$i]['id'] = $p->id;
$data[$i]['post'] = $p->post;
// tagsテーブル - count
$data[$i]['count'] = $p->tags_count;
foreach ($p->tags as $t) {
// tagsテーブル
$data[$i]['tags'][] = $t->tag;

}
$i++;
}

上記のように、件数は {リレーション名}_count として取得できます。

Array
(
[0] => Array
(
[id] => 1
[post] => post01
[count] => 3
[tags] => Array
(
[0] => tag01
[1] => tag03
[2] => tag01
)

)

[1] => Array
(
[id] => 2
[post] => post02
[count] => 1
[tags] => Array
(
[0] => tag07
)

)

[2] => Array
(
[id] => 3
[post] => post03
[count] => 1
[tags] => Array
(
[0] => tag01
)

)

[3] => Array
(
[id] => 4
[post] => post04
[count] => 0
)

[4] => Array
(
[id] => 5
[post] => post05
[count] => 0
)

[5] => Array
(
[id] => 6
[post] => post06
[count] => 0
)

[6] => Array
(
[id] => 7
[post] => post07
[count] => 0
)

[7] => Array
(
[id] => 8
[post] => post08
[count] => 0
)

[8] => Array
(
[id] => 9
[post] => post09
[count] => 0
)

[9] => Array
(
[id] => 10
[post] => post10
[count] => 0
)

)

Eagerロード

O/Rマッパーの最大の弱点に「意図せず連発されるクエリ発行」があります。ある一定の約束の元で動作するORMは、人間の意図を最大限読み取ってはくれません。素のSQL文を自分で書いたとして、1、2回のクエリ発行で済むところを、彼らは平気で何十回ものクエリを発行したりします。

例えば、idが1~10のデータを取得しようと思ったら、我々なら「where id IN ~」とすれば1回で取得できるようなところも、ORMでは「where id = 1」×10回とかして取得する場合が結構あり、負荷やオーバーヘッドを量産してくれる有難迷惑な現象だったりします。

よく「N+1問題」なんて言われますが、まさにこの、データ量N+1回のSQLクエリがORMによって走ってしまう現象の事を言います。

前置きが長くなりましたが、こういう事がないように、関連するモデルを一括で生成・取得を行うのがEagerロードです。

この記事で何度も出てくる以下のようなループ

$posts = $this->posts->all();
foreach ($posts as $p) {
$p->tags->tag;
}

実はこの場合には、それなりのクエリが発行されてしまっています。

全てのレコードを取得するのに1回、そしてループ中にtagに関するデータを取得するのに、記事の数だけクエリが発行されています。つまり、記事数が100なら100クエリ発行されているという事です。

ここで、with()メソッドを用いてEagerロードを行えば、クエリの発行は2回で済みます。

$posts = $this->posts->with('tags')->get();
foreach ($posts as $p) {
$p->tags->tag;
}

リレーションしているモデルが複数ある場合は配列で渡します。

$posts = $this->posts->with(['tags', 'users'])->get();

まとめ

ORMでのリレーションは、1対1など、モデル同士の関係を定義し結合を行っていくので、それぞれの関係を理解するまではなかなか飲み込みずらい部分もあります。

ただし、Laravelで提供されているEloquentに限らず、PHPフレームワークで提供されているORMでは同じ考えのもとにモデル定義を行っている事も多いので、1回理解してしまえば広く使えるようになります。

今回の記事で良く叩いたコマンド
php artisan migrate:reset
php artisan migrate:refresh
php artisan migrate
php artisan db:seed
php artisan db:seed --class=XxxxTableSeeder
php artisan make:model Models/Posts -m
php artisan make:seeder PostsTableSeeder
composer dump-autoload