リレーション

リレーションはテーブル同士の関連をモデルで表現し値にアクセスするための仕組みです。

目次

基本的なリレーション

基本的なリレーションの定義方法と使い方を学びましょう。

1対1

この例で使用されるテーブル定義は以下の通りです:

-------------------------
customers table
 int    : id
 string : name
 int    : group_id
 string : phone

 primarykey : id
 index      : group
-------------------------
addresses table
 int    : customer_id
 string : zip
 string : state
 string : city
 string : street

 primarykey : customer_id
-------------------------
groups table
 int    : id
 string : name

 primarykey : id
-------------------------

CustomerモデルにAddressの関連を定義します:

clss Address extends Model
{
}

class Customer extends Model
{
    public function address()
    {
        return $this->hasOne('App\Address');
    }
}

顧客の住所を取得します。

$address = $customer->address;
if ($address !== null) {
    echo $address->state;
}

Note: $customer->addressプロパティです! addressプロパティにアクセスした際に、addressメソッドが呼び出され、オブジェクトを取得し、プロパティにセットします。以下のようなコードが自動で実行されていると考えてください:

function __get($name)
{
    if ($name === 'address') {
         $relation = $customer->address();
         $model = $relation->get($this);
         if ($model !== null) {
              $customer->address = $model;
              return $customer->address;
         }
         return null;
    }
}

オブジェクトが見つからない場合はnullが返されます。

hasOne

hasOneメソッドのプロトタイプは以下の通りです:

hasOne(
        string                  $className,
        integer|string|string[] $foreignKey = null,
        string[]                $keyValuePropertyNames = null,
        boolean                 $optimize = false
      )

$className

関連先のモデルクラス名を、名前空間を含んだ完全な名前で指定します。

$foreignKey

$foreignKeyのデフォルト値は(小文字のモデル名) + _idです。この例の場合はcustomer_idです。フィールド名が規則に合わない場合は、以下のようにして指定します:

public function address()
{
    return $this->hasOne('App\Address', 'id');
}

addressesテーブルを検索するのに必要なのは、インデックス番号です。名前ではなくインデックス番号を指定することもできます。

public function address()
{
    return $this->hasOne('App\Address', 0);
}

$keyValuePropertyNames

検索には、primary keyのフィールドと同じ名前を持ったプロパティが使用されます。もし異なるプロパティを使用したい場合は、以下のようにプロパティを指定します:

public function address()
{
    return $this->hasOne('App\Address', 0, 'id');
}

1対多

顧客はどれか1つのグループに属しているとします。 Groupモデルは以下のようになります:

class Group extends Model
{
    public function customers()
    {
         return $this->hasMany('App\Customer');
    }
}

hasMany

hasManyメソッドのパラメータはhasOneメソッドのパラメータと同じです。

グループID1に属するすべての顧客を取得します。

$group = Group::find(1);
$customers = $group->customers;
echo count($customers);

オブジェクトが見つからない場合は、空のCollectionオブジェクトが返されます。

従属関係

hasOneおよびhasManyの逆の従属関係には、belongsToを使用します。

Note: belongsToメソッドの名前はモデルのプロパティ名と異なる名前にしなければなりません。

clss Address extends Model
{
    function customer()
    {
        return $this->belongsTo('App\Customer');
    }
}

class Customer extends Model
{
    // (省略)

    public function group()
    {
        return $this->belongsTo('App\Group');
    }
}

belongsTo

belongsToメソッドのプロトタイプは以下の通りです:

belongsTo(
          string                  $className,
          string[]                $keyValuePropertyNames = null,
          integer|string|string[] $otherKey = 'id',
          boolean                 $optimize = true
         )

$keyValuePropertyNames

検索には、(小文字のモデル名) + _idという名前を持ったプロパティが使用されます。もし異なるプロパティを使用したい場合は、以下のようにプロパティを指定します:

public function group()
{
    return $this->belongsTo('App\Group', 'group_id');
}

$otherKey

$otherKeyのデフォルト値はidです。異なる名前を使用したい場合は、以下のようにして指定します:

public function group()
{
    return $this->belongsTo('App\Group', null, 'code');
}

また、インデックス番号で指定することもできます。

public function group()
{
    return $this->belongsTo('App\Group', null, 0);
}

顧客が属するグループ名を表示

$customer = Customer::find(1);
echo $customer->group->name;

多対多

顧客に複数のタグをつけられるようにしてみましょう。 tagsテーブルにタグを用意しておきます。

多対多の関係を定義するには、中間テーブルを用意します。この例では、中間テーブルとしてtag_customerを使用します。

テーブル定義は以下の通りです:

-------------------------
tags table
 int    : id
 string : name

 primarykey : id
-------------------------
tag_customer table
 int    : tag_id
 int    : customer_id

 primarykey : tag_id, customer_id
 index : customer_id, tag_id
-------------------------

モデルは以下の通りです:

class Tag extends Model
{
}

// 中間クラスの定義が必要
class TagCustomer extends Model
{
    // テーブル名のsnake_case変換はサポートされていないため、直接指定する
    protected static $table = 'tag_customer';
}

class Customer extends Model
{
    // (省略)

    public function tags()
    {
        return $this->belongsToMany('App\Tag');
    }
}

belongsToMany

省略されたパラメータについて確認します。

belongsToMany(
               string                  $className,
               string                  $IntermediateClass = null,
               integer|string|string[] $foreignKey = null,
               string[]                $intermediatePropertyNames = null
             )

これらを明示的に指定した場合、以下のようになります:

class Customer extends Model
{
    // (省略)

    public function tags()
    {
        return $this->belongsToMany('App\Tag', 'App\TagCustomer', 'customer_id', 'tag_id');
    }
}

顧客につけられたタグの一覧を取得

$customer = Customer::find(1);
$tags = $customer->tags;
foreach($tags as $tag) {
    // (省略)
}

これはどのように処理されているのでしょうか?以下のコードをご覧ください:

// (これは疑似コードです)
class Customer extends Model
{
    // (省略)

    public function tags()
    {
        $relation = $this->hasMany('App\TagCustomer', 'customer_id', 'id');
        $tagCustomers = $relation->get($this);
        $results = array();
        $relation2 = TagCustomers::belongsTo('App\Tag', 'tag_id', 'id'); // (staticなbelongsToメソッドは実際には存在しません)
        foreach($tagCustomers as $row) {
            $results[] = $relation2->get($row);
        }
        $this->tags = $results;
    }
}

あるタグのついた顧客の一覧を取得

class Tag extends Model
{
    public function customers()
    {
        return $this->belongsToMany('App\Customer');
    }
}
$tag = Tag::find(1);
$customers = $tag->customers;
foreach($customers as $customer) {
    // (省略)
}

パフォーマンス

Lazy loading(遅延読み込み)

リレーションメソッドは、プロパティが読まれるときに初めてデータベースにアクセスします。

Eager loading

多くの場合は、遅延読み込みを使用すれば問題ありません。しかし、オブジェクトの数が多い場合は、データベースアクセス回数が多くなってしまう問題があります。

この場合、withメソッドを使うと、コレクション内のオブジェクトのリレーションを1回のデータベースアクセスで解決することができます。 withメソッドにはリレーションのメソッド名を指定します。

$customers = Customer::with('group')->all();
echo $customer[0]->group->name; // この行ではデータベースアクセスは発生しない

withはJOINのアルゴリズムと似ています。 Transactd PHP ORMでは、nested-loopsort-mergeの2つのアルゴリズムを使うことができます。これらは以下のように使い分けます:

$optimizeでアルゴリズムを選択することができます。

group_idは複数の顧客で同じことが多いので、sort-mergeを使います。

public function group()
{
    return $this->belongsTo('App\Group', 'group_id', 0, true /* sort-merge */);
}

住所は顧客ごとに異なるのでnested-loopを使います。

public function address()
{
    return $this->hasOne('App\Address', 0, 'id', false /* nested-loop */);
}

$optimizeパラメータはwithメソッドにのみ影響します。

Note: withfindXXfirstXXには使用できません。 getallメソッドにのみ影響します。

どちらのアルゴリズムを選択しても結果は同じです。パフォーマンスのみが異なります。

コレクションのEager loading

クエリーのメソッドチェーンでwithを使用するだけでなく、結果として受け取ったコレクションに対してEager loadingすることもできます。

コレクション内の各アイテムのリレーションを一度に読み取るにはloadRelationsを使用します。

$customers = Customer::all();
$customers->loadRelations(['address', 'group']); // address() と group()の関連をロードする

より簡単なリレーション定義

ここまでに説明したリレーション定義はLaravel 5に似ています。この方法は、ActiveRecordのテーブル定義規則に従っている場合には最善の方法です。

しかし、relationメソッドを使用すると、テーブル定義規則に関わらずより簡単にリレーションを定義可能です。 relationメソッドは、1対1、1対多、多対多、従属のすべての関係に使用することができます。

public function address()
{
    return $this->relation($className, $index, $keyValuePropertyNames, $optimize);
}

すべてのリレーションは$className$index$keyValuePropertyNamesを使って定義可能です。

belongsToManyリレーションは、addSubRelationメソッドを使用して以下のようにします:

class Tag extends Model
{
    public function customers()
    {
        return $this->relation('App\TagCustomer', 0, 'id')
                ->addSubRelation('App\Customer', 0, 'customer_id');
    }
}

ポリモーフィック・リレーション

ポリモーフィック・リレーションは、1つのテーブルと複数のテーブルとのリレーションです。

1対1のポリモーフィック

addressテーブルに、顧客の住所だけでなく、ベンダーの住所も格納します。

-------------------------
addresses table
 string : owner_type
 int    : owner_id
 string : zip
 string : state
 string : city
 string : street

 primarykey : owner_type, owner_id
-------------------------
vendors table
 int    : id
 string : name

 primarykey : id
-------------------------
class Vendor extends Model
{
    public function address()
    {
        return $this->morphOne('App\Address', 'owner');
    }
}

owner_typeフィールドには、owner_idのクラス名が名前空間付きで保存されています。

relationメソッドを使うこともできます。

class Vendor extends Model
{
    public function address()
    {
        return $this->relation('App\Address', 0, ['[App\Vendor]', 'id']);
    }
}

この例では、owner_typeフィールドには常に固定値App\Vendorを使用します。プロパティ名の代わりに固定値を使用する場合は、値を[]で囲みます。

逆のリレーション定義

clss Address extends Model
{
    function owner()
    {
        return $this->morphTo('owner');
    }
    // もしくは
    function owner()
    {
        // $className is unknown, so set to null.
        return $this->relation(null, 0, ['owner_type', 'owner_id']);
    }
}

住所に関連付くownerの名前を表示

$adresses = Address::all();
foreach($adresses as $address) {
   echo $address->owner->name;
}

owner_typeのようにクラス名をstring型で保持することは一般的ではありません。通常はクラスを表すintegerのコードで保存します。この場合、クラス名の配列を渡し、コードをクラス名に変換します。

// 'addresses'テーブルの'owner_type'フィールドがinteger型の場合
clss Address extends Model
{
    protected static $classTypeMap = [1 => 'App\Vendor', 2 => 'App\Customer'];
    function owner()
    {
        return $this->morphTo('owner', null, null, self::$classTypeMap);
    }
    // もしくは
    function owner()
    {
        // $classNameは不定なのでnullを渡す
        return $this->relation(null, 0, ['owner_type', 'owner_id'])
                   ->setMorphClassMap(self::$classTypeMap);
    }
}

1対多のポリモーフィック

1つの顧客またはベンダーに対して複数の住所を登録できるようにします。

-------------------------
addresses table
 int    : id
 string : owner_type
 int    : owner_id
 string : zip
 string : state
 string : city
 string : street

 primarykey : id
 index : owner_type, owner_id (duplicatable)
-------------------------
class Customer extends Model
{
    // (省略)

    public function address()
    {
         return $this->morphMany('App\Address', 'owner');
    }
    // もしくは
    public function address()
    {
        // インデックス番号が異なる
        return $this->relation('App\Address', 1, ['[App\Customer]', 'id']);
    }
}

逆のリレーションはmorphOneと同じです。

多対多のポリモーフィック

ベンダーにも複数のタグをつけられるようにしてみます。

-------------------------
taggables table
 string : taggable_type
 int    : taggable_id
 int    : tag_id

 primarykey : taggable_type, taggable_id, tag_id
 index      : tag_id, taggable_type, taggable_id
-------------------------
class Taggable
{
}

class Tag extends Model
{
    // このタグのついた顧客をすべて取得する
    public function customers()
    {
        return $this->morphedByMany('App\Customer', 'taggable', 'App\Taggable');
    }

    // このタグのついたベンダーをすべて取得する
    public function vendors()
    {
        return $this->morphedByMany('App\Vendor', 'taggable', 'App\Taggable');
    }

    // もしくは

    // このタグのついた顧客をすべて取得する
    public function customers()
    {
        return $this->relation('App\Taggable', 1, 'id')
               ->addSubRelation('App\Customer', 0, 'taggable_id');
    }

    // このタグのついたベンダーをすべて取得する
    public function vendors()
    {
        return $this->relation('App\Taggable', 1, 'id')
               ->addSubRelation('App\Vendor', 0, 'taggable_id');
    }
}

逆のリレーションは:

class Customer extends Model
{
    // (省略)

    function tags()
    {
        return $this->morphToMany('App\Tag', 'taggable', 'App\Taggable');
    }

    // もしくは

    function tags()
    {
        return $this->relation('App\Taggable', 0, ['[App\Customer]', 'id'])
               ->addSubRelation('App\Tag', 0, 'tag_id');
    }
}

class Vendor extends Model
{
    // (省略)

    function tags()
    {
        return $this->morphToMany('App\Tag', 'taggable', 'App\Taggable');
    }

    // もしくは

    function tags()
    {
        return $this->relation('App\Taggable', 0, ['[App\Customer]', 'id'])
               ->addSubRelation('App\Tag', 0, 'tag_id');
    }
}

リレーションに使用するプロパティのアクセス修飾子

モデル内のリレーションに使用されるプロパティはpublicアクセス可能である必要があります。これらのプロパティはRelationオブジェクトから読み書きされます。また、getterやsetterを介したアクセスでもかまいません。

class Tag extends Model
{
    protected $id; // getter/setterがpublicなのでこれはprotectedでもよい
    public function customers()
    {
        return $this->relation('App\TagCustomer', 0, 'id')
                ->addSubRelation('App\Customer', 0, 'customer_id');
    }
    public __get($name)
    {
         // $this->idがprotectedなので、リレーションのためにgetterはpublicでなければならない
         if ($name === 'id') {
              return $this->id;
         }
         return parent::__get($name); // Important for relationships.
    }
    public __set($name, $value)
    {
         // $this->idがprotectedなので、リレーションのためにsetterはpublicでなければならない
         if ($name === 'id') {
              $this->id = $value;
         }
    }
}

/* 以下はリレーションのためにpublicである必要がある:
   * Tag::id
   * TagCustomer::customer_id, tag_id (インデックス番号1のフィールド)
   * Customer::id
*/