リレーション
リレーションはテーブル同士の関連をモデルで表現し値にアクセスするための仕組みです。
目次
基本的なリレーション
基本的なリレーションの定義方法と使い方を学びましょう。
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-merge
false
: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
*/