【Laravel】Eloquentで複数のテーブルを参照する

 EloquentはLaravel中で用意されたモデルのベースクラスであり、1Eloquentに対して1テーブルがマッピングされることを期待しています(1テーブルに複数のEloquentがマッピングされるのはOK。むしろそれを許さないと巨大クラスが誕生の兆しを見せます)。これのためEloquentインスタンスの持つプロパティ名にはマッピングされたテーブルの持つカラム名が、プロパティの値にはレコードの持つプロパティ名のカラムの値が入っていることが期待されます。これは言い換えればEloquentを介して発行されるSQLクエリはいずれも次の様にFROM句が単純な一テーブル参照であることを期待しているということです。

FROM tbl_name

 しかし実際のクエリ発行においては FROM tbl_name のみで全てのデータを扱うことは現実的に困難です。ほぼ間違いなく JOIN 等も必要になります。もし単一の FROM のみで済むように実装するならば正規化を無視したテーブル設計を行うか、相当数のクエリを発行する必要があります。
 これを解決するためには特定のFROM句を持つモデル、ないしモデルを統合するリポジトリクラスを追加するのがよいです。
 モデルを追加する場合の考え方はSQLのViewとほぼ同じです。Viewは実体を持たないが参照を持つ便利なテーブルを用意するやり方で、モデルを追加するやり方はこのViewをモデル上で作るという考えです。この記事ではこちらで進めます。メリットは簡易に実装できる点、デメリットは1モデル=1テーブルを1モデル=1FROM句に拡大することによるわかりにくさです。余談ですが、信頼できるマテリアライズドビューを実装しているRDBを扱うならば、マテリアライズドビューを作ってそれにEloqeuntを結び付けて1モデル=1テーブルという一つの考えを一貫させた方がわかりやすく、作りやすく、バグりにくくでお得です。
 リポジトリクラスを追加するやり方はドメイン駆動設計における集約の考え方を用います。集約は大雑把に言えば次図(Eric Evans. エリック・エヴァンスのドメイン駆動設計 (Kindle の位置No.3134). 翔泳社. Kindle 版. より引用)にあるような一貫して関連し続ける一塊のことです。この考えを用いるデメリットはリポジトリクラスが大量に増える点です。メリットはフレームワークを変えやすい(フレームワークの用意したモデルと疎結合になる)、RDB以外のモノも並行して扱いやすい、といった大規模で複雑な開発にも耐えうる点です。

 Eloqeuntの実装例です。まず集約の範囲をFROM句で表現します。ここから例には↑図で表現されているモノを用います。

FROM 自動車
         JOIN 車輪
         JOIN タイヤ
         JOIN 位置

 LEFT、RIGHT、INNER、ONは実情に合わせて適宜変えればOKです。これをEloqeuntに反映させます。Eloquentのクエリ発行フローは必ず\Illuminate\Database\Eloquent\Builder::setModelを経由します。このsetModelでFROM句を規定します。実装が次です。

    /**
     * Set a model instance for the model being queried.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return $this
     */
    public function setModel(Model $model)
    {
        $this->model = $model;

        $this->query->from($model->getTable()); // ここでfromを決定.
        // getTableの製作者はstring型が返されることを期待してgetTableメソッドを作ったっぽいのでこの記事のやり方は許されなくなるかもしれません

        return $this;
    }

 モデルのgetTableメソッドにfromメソッドに渡してよい形でJOIN付きFROMを渡せばOKです。fromメソッドの実装は次です。

    /**
     * Set the table which the query is targeting.
     *
     * @param  \Closure|\Illuminate\Database\Query\Builder|string  $table
     * @param  string|null  $as
     * @return $this
     */
    public function from($table, $as = null)
    {
        // つまりサブクエリをFROMにできる
        if ($this->isQueryable($table)) {
            return $this->fromSub($table, $as);
        }

        $this->from = $as ? "{$table} as {$as}" : $table;

        return $this;
    }

 このため次の様にサブクエリを発行するコードを仕込むとEloquent参照対象のFROMがサブクエリ結果のSELECTになります。

    public function getTable()
    {
        return \DB::query()->select([
            '自動車.id as id',
            '車輪.name as wheel_name',
            'タイヤ.name as tire_name',
        ])
            ->from('自動車')
            ->join('車輪')
            ->join('タイヤ')
            ->join('位置')
    }

 これで複数テーブルを参照するEloquentを作れます。ちなみにシャレにならない問題点にサブクエリが絶対発行される点があります。サブクエリはインデックスが効かない、実質クエリを二回発行することになる、とクエリの実行速度が遅くなりやすいです。もう一つは複数テーブルを参照するため、素のEloquentでは DELETE 構文に関連している部分が壊れ、新規実装する必要があります。fill->saveやupdateの際も、テーブル指定を忘れない様に引数の配列キーを作っておく必要があります。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.2.2 DELETE 構文
 一応どうにか\Illuminate\Database\Eloquent\Builder::setModelを上書きできれば、サブクエリを発行できずに済みます。いい感じに拡張したいものです。

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG