リレーション
リレーションはテーブル同士の関連をモデルで表現し値にアクセスするための仕組みです。
目次
基本的なリレーション
基本的なリレーションの定義方法と使い方を学びましょう。
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-loopとsort-mergeの2つのアルゴリズムを使うことができます。これらは以下のように使い分けます:
- 元の値に重複がある場合:
sort-merge - 元の値がユニークな場合:
nested-loop
$optimizeでアルゴリズムを選択することができます。
true:sort-mergefalse:nested-loop
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: withはfindXXやfirstXXには使用できません。 getやallメソッドにのみ影響します。
どちらのアルゴリズムを選択しても結果は同じです。パフォーマンスのみが異なります。
コレクションの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を使って定義可能です。
$classNameはモデルクラスの名前です。$indexは$classNameのテーブルを探す際に使用するインデックス番号です。$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
*/