Laravel 5.1 入門記 その15 (Eloquent リレーション編)
今回は何気なくすっ飛ばしていた、Eloquent のリレーション定義の回です。
オフィシャルドキュメントではこちら。
Eloquent: Relationships - Laravel - The PHP Framework For Web Artisans
リレーションの種類
Laravel で用意されているリレーション定義は、
- One to One
- One to Many
- Many to Many
上記の3つで、まぁ割と一般的。双方向の定義もあるので、実際の定義はもうちょっと増えます。
では順々にリレーション定義を試していきます。
One to One
見出しの通り 1 対 1 の関連を定義する方法です。オフィシャルドキュメントの例をそのまま使います。
User は 1 個の Phone を持っている状態を表現します。 User クラスの phone メソッドに users テーブルと phones テーブルの関連を定義します。
<?php namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * Get the phone record associated with the user. */ public function phone() { return $this->hasOne('App\Phone'); } }
逆順になってしまうけど、これを DDL に定義すると
create table users ( id auto_increment, name varchar(50), created_at timestamp, updated_at timestamp ); create table phones ( id auto_increment, user_id integer, phone_number varchar(20), created_at timestamp, updated_at timestamp )
こんな感じです。(シンタックス間違ってるかも)
users テーブル側には phone_id は持たず、phones テーブル側に user_id カラムを持っているのがポイントです。
ちなみにこれは Eloquent のルールに沿っているから定義が短くて済みますが、カラム名が Eloquent のルールに沿っていない場合は外部キーのカラム名は次のように手動で設定します。
<?php namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * Get the phone record associated with the user. */ public function phone() { return $this->hasOne('App\Phone', 'user_id'); } }
hasOne で指定したモデルクラス名の次にカラムを指定します。
続いて、 Phone 側から User へのリレーションを定義するパターン。
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Phone extends Model { /** * Get the user that owns the phone. */ public function user() { return $this->belongsTo('App\User'); } }
users テーブルには phone_id を持たず、phones テーブルに user_id (外部キー)を持つ場合には hasOne ではなく、 belongsTo を使います。この belongsTo も Eloquent の命名規則に沿ってないテーブルに対応できます。
わかりやすくするために、テーブル定義も変えます。
create table users ( user_id auto_increment, name varchar(50), created_at timestamp, updated_at timestamp ); create table phones ( id auto_increment, owner_id integer, phone_number varchar(20), created_at timestamp, updated_at timestamp )
users テーブルのプライマリキーを id から user_id に変更し、phones テーブルの user_id を owner_id に変更しました。
これを belongsTo で表現すると…。
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Phone extends Model { /** * Get the user that owns the phone. */ public function user() { return $this->belongsTo('App\User', 'owner_id', 'user_id'); } }
このように belongsTo の第 2 引数に phones テーブル上の外部キーカラムを、第 3 引数に users テーブルのキーを指定します。順序がややこしいので注意して指定してください。慣れるまでは、SQL ログを見ながら調整していくと良いと思います。
One to Many
One to Many の場合は結果が複数取れてくるパターンのリレーションです。これもとりあえずは、オフィシャルドキュメントのサンプルで。
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { /** * Get the comments for the blog post. */ public function comments() { return $this->hasMany('App\Comment'); } }
これだけ見ると分からないので、DDLも。
create table users ( id auto_increment, name varchar(50), created_at timestamp, updated_at timestamp ); create table comments ( id auto_increment, user_id integer, comments varchar(100), created_at timestamp, updated_at timestamp )
こんな感じ〜
あれ、One to One の Phone の時と変わってなくない?手抜きじゃ無い?その通りです。
One to One の hasOne と One to Many の hasMany はあまり大きな違いは無く、それはまさにその通り。オブジェクトが一個返るか、コレクションで複数返るかの違いしか無いです。ちなみに One to Many の逆方向のリレーションは One to One と同じく belongsTo で定義します。
hasMany を使った定義も、Eloquent のルールから外れたテーブル定義に対応できるようになっており、次のようになります。
まずは DDL 。
create table users ( user_id auto_increment, name varchar(50), created_at timestamp, updated_at timestamp ); create table comments ( id auto_increment, editor_id integer, comments varchar(100), created_at timestamp, updated_at timestamp )
users テーブルのプライマリキーを id → user_id に変更、comments テーブルは user_id → editor_id に変更します。これを hasMany で表現すると…
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { /** * Get the comments for the blog post. */ public function comments() { return $this->hasMany('App\Comment', 'editor_id', 'user_id'); } }
このように、第2引数と第3引数に変更したカラムを設定します。
One to One と One to Many は似たところもあるので、慣れればどうってこと無いですね。
Many to Many
多対多の関連に入ります。これもサンプルを考えるのが面倒なので、オフィシャルドキュメントをそのままに。
出てくるテーブルは、 users, roles, role_user の3テーブルで、このうち role_user は users と roles を結び付ける中間テーブル (pivot table) となります。オフィシャルドキュメントには DDL が無いので、今回もまずは DDL から。ちゃんと検証して書いてないので、エラー出てもテヘペロでごまかします。
create table users ( id auto_increment, name varchar(50), created_at timestamp, updated_at timestamp ); create table roles ( id auto_increment, name varchar(50), created_at timestamp, updated_at timestamp ); create table role_user ( id auto_increment, role_id integer, user_id integer );
これを User 側から Role を取得するようなリレーションを定義すると次のようになります。
<?php namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * The roles that belong to the user. */ public function roles() { return $this->belongsToMany('App\Role'); } }
Eloquent の命名規則に沿っていると凄く楽! 反面、命名規則からずれると、超面倒くさい状態に陥ります。
DDL をいじります。
create table users ( id auto_increment, name varchar(50), created_at timestamp, updated_at timestamp ); create table roles ( id auto_increment, name varchar(50), created_at timestamp, updated_at timestamp ); create table pivot_user_roles ( id auto_increment, pivot_role_id integer, pivot_user_id integer );
主に中間テーブルを変えてみました。これをモデルのリレーションに定義します。
<?php namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * The roles that belong to the user. */ public function roles() { return $this->belongsToMany('App\Role', 'pivot_user_roles', 'pivot_user_id', 'pivot_role_id'); } }
こんな感じです。
複雑なリレーションモデルになってきたので、 Eloquent の命名規則からちょっと外れただけで、手動で設定するパラメータが一気に増えました。
ちなみに中間テーブルに作成・更新のタイムスタンプを持たせるようなことも可能で、リレーション定義のところで定義を追加します。
<?php namespace App; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * The roles that belong to the user. */ public function roles() { return $this->belongsToMany('App\Role', 'pivot_user_roles', 'pivot_user_id', 'pivot_role_id') ->withTimestamps(); } }
withTimestamps を追加することで、created_at updated_at が追加されます。タイムスタンプ以外のカラムが中間テーブルに存在する時には、pivotWith でカラムを指定したり、wherePivot でヒットする条件を設定したり(中間テーブルの削除フラグチェックを条件に入れたりとか…)、Illuminate\Database\Eloquent\Relations\BelongsToMany に実装があります。この辺りのドキュメントが Laravel は用意されていないので、コードを読んで調べるのが早いと思います。
多対多のリレーションは定義が面倒ですが、根気よく調べれば、割とやりたいことは出来ると思います。
Has Many Through
多対多の変形パターンです。登場するのは、Country と User 、 Post の3つ。 Country は単独で存在し、 User は所属する国を country_id を持ち、Post は user_id として、書き込みしたユーザ情報を持ちます。この時に、Country と Post は直接の関連は持たないけど、ある国のユーザの書き込みを参照したいと言う時の定義。
書いてて訳分からなくなってきた。また DDL から書きます。
create table countries ( id auto_increment, name varchar(50), created_at timestamp, updated_at timestamp ); create table users ( id auto_increment, country_id integer, name varchar(50), created_at timestamp, updated_at timestamp ) create table posts ( id auto_increment, user_id integer, comment varchar(50), created_at timestamp, updated_at timestamp );
こんな感じで、 posts テーブルに country_id があれば今までのやり方ができるけど、直接の参照が無いので users を中間テーブルに見立てて Post を取得する定義は以下のようになります。
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Country extends Model { /** * Get all of the posts for the country. */ public function posts() { return $this->hasManyThrough('App\Post', 'App\User'); } }
こんな感じです。第1引数に Post を、第2引数に中間テーブルとなる User を指定します。これで以下の様に、User を意識しないで、国に紐付いた書き込みを取得できます。
for (Country::find(1)->posts as $post) { echo($post->comment); }
うーん、って思ったけど意外と使い道ありそう。
ちなみにこのパターンでも、Eloquent の命名規則に沿わない場合は個別に設定できて
class Country extends Model { public function posts() { return $this->hasManyThrough('App\Post', 'App\User', 'country_id', 'user_id'); } }
こんな感じになります。なんだか順序がメチャクチャで分かりにくいので注意しましょう。
ということで、リレーションについては、ここまで。 他にも Polymorphic Relation とかもありますが、ここで切り上げます。
Eloquent で hasMany や belongsToMany を定義しておくと、追加・更新処理が非常に簡単に書けます。面倒なので、たぶん記事には起こさないけど、特に sync は便利でした。更新系の処理は画面と一緒に流れを見た方が分かりやすいので、Laracast の動画を見るのもオススメです。
これの 21 〜 24 あたりを見ると、一通り使いこなせるようになると思います。
これで、Laravel 入門記も一区切り。2015年11月には、技術評論社のムック本を書いた人たちが執筆中の Laravel 5.1 の日本語の書籍が出るらしいので、それに期待しましょう。