日記

日々のことと、Python/Django/PHP/Laravel/nodejs などソフトウェア開発のことを書き綴ります

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 の動画を見るのもオススメです。

laracasts.com

これの 21 〜 24 あたりを見ると、一通り使いこなせるようになると思います。

これで、Laravel 入門記も一区切り。2015年11月には、技術評論社のムック本を書いた人たちが執筆中の Laravel 5.1 の日本語の書籍が出るらしいので、それに期待しましょう。

www.amazon.co.jp