クエリービルダーとEloquent

タグ: Laravel4  

クエリービルダーとEloquent ORM

これはLaravelアドベントカレンダーの24日目です。

残念ながら、Laravelアドベントカレンダーくらい、面白くていいんじゃないかと思い、おちゃらけてみましたが、一人で浮いてしまいました。そこで、この傷ついた心を癒やすため、多少真面目に解説したいと思います。

クエリービルダーとEloquentを触り始めた人のために、両方の仕組みの基本をさらっと解説します。ちょこっと自分でもいじってみた人向けです。

クエリービルダー

クエリービルダーは「クエリーを組み立てるもの」という意味です。この場合の「もの」はクラスです。ですから、クエリーを組み立てるために使用するクラスを意味しています。コーディングするときのクラス名は簡単にDBになっています。Laravelのクエリービルダーには、一応Fluentという名前があったのですが、バージョン4からまったく聞かなくなりました。自然消滅です。

クエリーは問い合わせのことです。もちろんSQLで使われる用語の「クエリー」です。

自前でSQLを書いても良いのですが、SQLエンジンによる方言が多少ありますので、そうした差異をビルダーは吸収してくれます。また、SQL言語とPHP言語は異なった文法です。いろいろな言語を経験している方ならともかく、多くのプログラム言語に親しんでいない方には、こうした複数言語を取り扱うのは負担になります。クエリービルダーを使えば、SQL操作的な考えは必要ですが、PHP言語から離れることなく、SQLを発行できます。つまり、「SQLを実行したいけど、SQL言語って苦手なんだよなぁ。PHPぽく書きたいなぁ。」というクラスです。

厳密に言えば、問い合わせ以外のSQL操作も組み立てるので、クエリーに限っていませんが、元々のSQLでもクエリーという場合、そうしたSQL操作を含める場合もあります。ですから、「クエリー」ビルダーという名前で納得して下さい。

さて、クエリービルダーが一番使われるのはもちろんクエリー、問い合わせです。SQLで言えばSELECT文ですね。

PHPでDBエンジンを使用する場合、最近はPDOを使うのが主流です。安全性と速さを兼ね備えていますからね。Laravelも、もちろん内部でPDOを利用しています。

簡単なりんごテーブルを考えましょう。applesテーブルです。

フィールド 説明
id INTEGER 主キー
name VARCHAR りんご名
create_at DATETIME 作成日時
update_at DATETIME 更新日時

このテーブルを全件取得するには次のように書きます。

$results = DB::table('apples`)->get();

table()でテーブル名を指定します。get()でSELECT実行です。もし条件を付けたければwhere()メソッドなど豊富に用意されています。

getで実行するまで、クエリーは実行されません。ですからメソッドを使って動的に組み立てることも可能です。文字列操作でSQL文を生成するよりも、クエリービルダーのメソッドを利用したほうが、わかりやすく書けます。これは、クエリービルダーを使う利点の一つです。

結果の$resultsはstdClassの配列です。このSELECTの結果はPDOの定数で指定したものです。app/config/database.phpの最初の設定オプションです。

'fetch' => PDO::FETCH_CLASS,

PDOの取得オブジェクトの指定は、http://www.php.net/manual/ja/pdo.constants.phpに記述されている多くの取得方法が利用できます。使い所が難しい物もありますね。配列の要素がstdClassなのが気に入らない、配列のほうが良い人という人ならば、PDO::FETCH_ASSOCなどへ指定を変更できます。後ほど紹介する、ORMのEloquentも併用したい人は、デフォルトのままにしておきましょう。

りんごテーブルは、いわゆる典型的な「マスター」テーブルです。コード、この場合はIDですが、そのコードに対する情報を保存しています。

マスターに対する、「トランザクション」テーブルも考えましょう。りんごの注文テーブルです。apple_ordersテーブルです。今回は作成日時を受注日時として考えましょう。

フィールド 説明
id INTEGER 主キー
apple_id INTEGER りんごID
amount INTEGER 注文量
create_at DATETIME 作成日時
update_at DATETIME 更新日時

こちらも全レコードを取得するには、条件を付けずにそのままget()します。

$results = DB::table('apple_orders')->get();

$resultsの結果も、上記のテーブル構造をそのままstdClassのプロパティーにし、全件分レコードが配列になります。でも、せっかくりんごマスターがあるのですから、やっぱり数字のりんごIDでなくて、人間が読めるりんご名をレコードに含みたいですよね。

LEFT JOINでマスターを引っ張ってきましょう。レコードに名前を含めましょう。

$results = DB::table( 'apple_orders' )
    ->leftJoin( 'apples', 'apple_orders.apple_id', '=', 'apples.id' )
    ->select([ '*', 'apple_orders.created_at as ordered_at' ])
    ->get();

クエリービルダーでSELECTする項目名に別名を付けたいときは、"AS"を含めて項目を指定します。(普段使わないので、ずっと忘れていました。別名をつける方法が見つからずに苦労している人は、結構いると思います。こんな方法ですよ。:D)

フィールドに別名をつける場合は'フィルード名 as 別名'とSQLそのままで指定します。また、SQLのSELECT文と同様に、結果に含めるフィールドを指定することも可能です。全フィールドを表す'*'も利用できます。

今回はJOINするテーブル両方に同じ名前の項目(作成と更新時の日時フィールド)があるため、SELECT結果のcreated_atとupdate_atの結果は、JOINする方のapplesテーブルの内容になってしまいます。今回は手抜きで、注文テーブルの作成日時(created_at)を注文日時として取り扱うことにします。このフィールドが消えてしまうと困るので、別名(ordered_at)を付けています。

取得結果には、りんご名も含まれています。めでたし、めでたし。後はforeachなり、array_walk関数なりで、処理してください。

var_dump()の出力例:

array (size=6)
  0 =>
    object(stdClass)[131]
      public 'id' => int 1
      public 'apple_id' => int 1
      public 'amount' => int 10
      public 'created_at' => string '2013-12-15 07:52:11' (length=19)
      public 'updated_at' => string '2013-12-15 07:52:11' (length=19)
      public 'name' => string 'ふじ' (length=6)
      public 'ordered_at' => string '2013-12-15 07:55:43' (length=19)
  1 =>
    object(stdClass)[132]
      public 'id' => int 1
      public 'apple_id' => int 1
      public 'amount' => int 5
      public 'created_at' => string '2013-12-15 07:52:11' (length=19)
      public 'updated_at' => string '2013-12-15 07:52:11' (length=19)
      public 'name' => string 'ふじ' (length=6)
      public 'ordered_at' => string '2013-12-15 07:55:43' (length=19)
  2 =>
    object(stdClass)[133]
      public 'id' => int 2
      public 'apple_id' => int 2
      public 'amount' => int 12
      public 'created_at' => string '2013-12-15 07:52:11' (length=19)
      public 'updated_at' => string '2013-12-15 07:52:11' (length=19)
      public 'name' => string 'ジョナ' (length=9)
      public 'ordered_at' => string '2013-12-15 07:55:43' (length=19)
      ...

Eloquent ORM

Eloquentです。ORMです。

Eloquentは「雄弁」という英単語です。話術に長けていることです。もちろん、このORMに名付けられていなければ、一生覚えることはないような単語です。読みやすさからつけた名前でしょう。

ORMはコンピューター用語ですね。Object-relational mappingの略です。Wikidediaによるとオブジェクト関係マッピングだそうです。説明は長いです。いつも説明が長い私が言うのですから、大したものです。

簡単に言えば、テーブルのレコードと、プログラム言語の扱うオブジェクトを対応付けることです。Eloquentはアクティブレコードのパターンを利用していると説明されていますが、これは1レコードを1オブジェクトに対応させることです。

まず、LaravelのEloquent ORMのオブジェクト、通称Eloquentモデルをご覧ください。applesテーブルに対応する、Appleモデルです。

<?php

class Apple extends Eloquent
{
    protected $guarded = ['id' ];

    public function appleOrders()
    {
        return $this->hasMany( 'AppleOrder' );
    }

}

Eloquentモデル名は単数形のクラス名、それに対応するテーブルは複数形というデフォルトの規約があります。ORMモデルのオブジェクトが、テーブル上の1レコードを表しますので、モデル名は英語なら単数形です。それに対応するテーブルは複数レコード存在するのが通常ですので、複数形にします。ですから、これは規約というより、多くの英語を話す人たちにとって自然な命名規則です。もちろん、この規約から外れる名前をつけることも可能です。

単純です。ORMというとテーブルのフィールドと、クラスのプロパティーを結びつけるコーディング、正にマッピングを記述するものが多いですね。私は、それが嫌でORMは使ってきませんでした。ちょっと触っては、止めてを繰り返していました。LaravelのEloquentで初めて本格的に使用し始めました。一番の理由は、直接マッピングしなくていいからです。だって、テーブル直したら、クラスも合わせて直す必要があるのは二度手間です。まあ、確かに、プログラミンではよくこうした手間はつきものですね。でも、スキーマの定義とマッピングの定義で、2箇所に別れてしまうという感覚が嫌なのでした。

Eloquentでは、そうしたフィールドのマッピングは必要ありません。読み込んだレコードの構造がそのまま、モデルのプロパティーとしてアクセスできます。連想配列としてもアクセスできます。よく言えば単純なんです。悪く言えば、PDOで取り込んだ結果とさほど違いません。この単純であることが、Eloquentを使いやすくしています。

Eloquent ORMではモデルが含むフィールドを管理しません。取得結果に含まれているフィールドがそのままプロパティー、もしくは連想配列のキーとして指定しアクセスできます。逆にテーブルに含まれないフィールド名のプロパティー、もしくは連想配列のキーを指定することも可能です。ただし、テーブル上にフィールドが存在しないのでsaveメソッドで書き戻しても、もちろん保存されることはありません。存在しないフィールドを保存しようとするので、SQLのエラーになります。

$guardedというプロパティを定義してありますが、わからない方は今回無視して下さい。マッピングではありません。

その下は、リレーションの定義です。関連付けの定義です。他のテーブルとの関係を定義しています。りんごテーブルの1レコードは、りんご一種類を表現しています。同じりんごに対して注文は複数件あることでしょう。テーブルのレコードを人だと思って下さい。関連を所有で表します。すると、りんごテーブルは、いくつかの注文を持っていることになります。複数の注文です。(もちろん、不人気な商品での注文0の時もあるでしょう。1回だけということもあるでしょう。しかし、この場合の「複数」は1回、0回を含むものと考えましょう。)

$this->hasMany('AppleOrder')は、「この(this)モデルは、AppleOrderモデルを複数所有する(has many)」を表しています。AppleOrderモデルとは、もちろんapple_ordersテーブルのレコード1件を表す、Eloquentモデルのことです。

<?php

class AppleOrder extends Eloquent
{
    protected $guarded = ['id'];

    public function apple()
    {
        return $this->belongsTo('Apple');
    }

}

Appleモデルとよく似ています。何もマッピングしていません。違いは関連付けが逆になっていることで、$this->belongsTo('Apple')です。belongsToは「〜に所属する」です。りんごモデルはりんご注文を所有していますが、りんご注文モデル1件から見れば、あるりんごモデル1件に所属しています。それを表しています。

実際の動作で考えれば、何を意味しているのかもっとわかりやすくなります。ある一つのAppleモデルでappleOrders()メソッドが呼ばれたら、それはAppleOrderモデルが表すapple_ordersテーブルのapple_idと、Appleモデルのidフィールドの値が一致するレコード(複数)を表します。

逆にある一つのAppleOrderモデルでapple()メソッドが呼び出されたら、自身のapple_idと一致するappplesテーブルのレコード(一件)を意味します。

以下のコードで感覚を掴んで下さい。

// 注文全件の取得
$orders = AppleOrder::get();

// 条件付けで注文を取得
$orders = AppleOrder::where('create_at', '>', '2013-07-04 12-34-56')->get();

// 注文番号(id)により、一件取得
$order = AppleOrder::where('id', '=', $id)->first();

// 上記と同じ働きをするコード
$order = AppleOrder::find($id);

// 定義した関連付けを利用し、対応したりんごモデルを取得(1件)
$apple = AppleOrder::find($id)->apple()->first();

// 上記と同じ働きをするコード
$apple = AppleOrder::find($id)->apple;

結果が複数になる可能性がある場合はget()で閉めます。結果はモデルの配列です。実際にクエリーが実行され、結果が取得されます。これを最後に付けないとクエリーは実行されません。同様に、結果が一件だけの場合、もしくは複数でも最初の一件だけ取得できればよいのなら、first()で取得です。この場合は配列ではなく、モデルがそのまま取得できます。

Eloquentを理解する最初の一歩は、getとfirstで何がリターンされるのかを理解することです。Eloquentの関連付けを理解する最初の一歩も、各文法を書いた時に何がリターンされるのかを理解することです。リターンされるものをデバッガやvar_dump、dd()で確認し、実際にどのオブジェクトが戻ってくるのか確認してみましょう。それが把握できれば、Eloquentを便利に使えるようになります。

最後の例は、メソッド名appleをプロパティのように指定しています。それにより注文を起点にして、りんごテーブルのレコード1件分、つまりAppleオブジェクトのインスタンス(モデル)が取得できます。$apple->nameで、りんごの名前が取得できます。

同様に、りんごモデルを起点に考えてみましょう。以下のようにモデルを取得する方法を考えて下さい。

  • りんご全件の取得
  • 名前が「ふじ」のレコード1件の取得
  • idが2のレコード一件の取得
  • apple_idに1を持つ全注文の取得

もちろんやり方はいろいろあります。最後の問題はもちろん、注文モデルを直接取得するのではなく、りんごモデルを起点にし、関連で取得して下さい。

解答の一例です。

// りんご全件取得
$apples = Apple::get();

// 名前が「ふじ」のレコード一件の取得
$apple = Apple::where('name', '=', 'ふじ');

// idが2のレコード一件の取得
$apple = Apple::find(2);

// apple_idに1を持つ全注文の取得
$orders = Apple::find(1)->appleOrders;

さて、Eloqunetには直接クエリービルダーが記述できます。実際に先ほどのLEFT JOINをEloquentで行ってみましょう。

$orders = AppleOrder::leftJoin( 'apples', 'apple_orders.apple_id', '=', 'apples.id' )
        ->get([ '*', 'apple_orders.created_at as ordered_at' ]);

このようにEloquentモデルにクエリービルダーを記述する場合、Eloquentモデルは、単にメインの操作対象テーブルを表すだけだと考えましょう。

この注文全件のレコードには、Appleテーブルから引かれたnameも、別名を付けた注文日付のordered_at含まれています。selectメソッドは使用していませんが、実はgetメソッドでも項目の選択もできます。

この結果をデバッガーやvar_dumpでみてもらうとわかりますが、結果はAppleOrderクラスのオブジェクトになっています。この節の最初に説明した通り、EloqunetはSELECTの結果に基づいてフィールドを扱うだけなのです。元のテーブルに何が含まれているかどうかなど、気にかけません。

私達使用者も、$ordersがどんなクラスのインスタンスの配列なのかには関心はありません。要は結果に希望のフィールドの取得結果が存在していれば、それで良いわけです。必要に応じて、Eloqunetクラスが提供するメソッドや、コレクションとしての操作ができれば満足です。クラスの名前がりんごでもみかんでもバナナでもそんなことはどうでも良いのです。

まあ、何クラスに関心がないのは取得時です。更新や追加するなら、関心を持たないとダメですけどね。取得時は名前が何であれ必要なフィールドが存在していれば、みなさんも満足できるでしょう。

逆に、メモリを節約するためにモデルオブジェクトに含めるフィールドを制限することもできます。例えばりんご全件のIDと名前のみ持つAppleモデルの配列を取得するには、次のようにします。

$apples = Apple::get(['id', 'name']);

これでりんごモデルなのですが、テーブルには存在している更新と変更の日時項目を持っていないAppleモデルの配列が取得できます。モデルに含まれていないので、例えば、$createdAt = $apples[1]->created_atなどのように、日時項目へアクセスすると、nullが返ってきます。

Eloqunetモデルから存在しないフィールドの値を取得しようとするとnullになります。初めの頃はフィールド名を間違えた場合などで、はまりがちです。通常、オブジェクト上に存在しないプロパティーにアクセスすると、エラーになりますが、Eloquentではnullが返ります。

さて、Eloquentモデルへクエリービルダーも記述できるのですが、やはりこれはあまりORMっぽくありません。SQLの香りがプンプンします。PHPのオブジェクト操作というよりは、SQLのテーブル操作そのものです。テーブル結合はテーブルの結合です。オブジェクトの結合ではありません。

Eloquentの機能を活かすには、Eagerロードを使用します。Eagerとは切望する、熱望するという意味です。すでにEagerロードは、ORMの用語として定着しています。

$orders = AppleOrder::with('apple')->get();

withメソッドに関連付けのメソッド名を指定します。すると、その関連付けからAppleレコードを取得し、結果のオブジェクトに含めます。AppleOrderオブジェクト全件分の配列と、それに対応するAppleオブジェクトの配列を結果の$ordersが持っているとイメージして下さい。

Eagerロードの結果に含まれるのは、デフォルトで元のレコードに関連あるレコードだけです。例えば今回の場合、AppleOrderに含まれているのが、りんごテーブルのIDで1,2,3のものであれば、含まれるAppleオブジェクトは1、2,3のレコードです。それ以外のレコードは含まれません。

これを実現するため、EloquentはSQLでWHERE IN構文を利用します。つまり必要なIDを列記します。SQLはエンジンの制限や設定でSQL分の長さが決まっています。これを超えないようにしましょう。

apple_ordersテーブルにもともと含まれているフィールドには、直接プロパティーとしてアクセするように記述します。applesテーブルに含まれているフィールドへアクセスするには、appleというAppleOrderモデルで定義したリレーション(関連付け)を表すメソッド名を経由します。

foreach($orders as $order)
{
    echo $order->amount; // 注文テーブルのフィールド
    echo $order->apple->name; // りんごテーブルから
    echo $order->create_at; // 注文テーブルのフィールド
    echo $order->apple->create_at; // りんごテーブルから
}

LEFT JOINを使用したときは、フィールド名がバッティングしたため消えてしまうcreate_atフィールドを別名として指定しましたが、EloquentのEagerロードでは、applesテーブルのフィールドはそのまま含まれます。アクセスもできます。Appleモデルのコレクションとして内部に含まれているからです。そのため、名前を変える必要もありません。

Eloquentで取得した結果は、Laravelのコレクションです。ドキュメントに明示してありますね。実際にどんなメソッドが使えるのかを確認するためにはCollectionクラス(vendor/laravel/framework/src/Illuminate/Support/Collection.php)を確認するのが一番です。

テーブルの取得結果からEloquntモデルへの変換

テーブル取得結果、stdClassの配列を、Eloquentモデルに変換するために、Laravelは何を行っているのでしょうか。ポイント抜き出してみましょう。

クエリービルダーのところで使ったLEFT JOIN文を再度ご覧ください。

$results = DB::table( 'apple_orders' )
    ->leftJoin( 'apples', 'apple_orders.apple_id', '=', 'apples.id' )
    ->select([ '*', 'apple_orders.created_at as ordered_at' ])
    ->get();

select()の内容をget()に移動すれば、SELECT文一文省略できるのは、もう理解できたかと思います。

さて、$resultはstdClassの配列です。そしてLEFT JOINの結果ですから、マスターテーブルやトランザクションテーブルのような恒久的な保存テーブルではありません。一時的なテーブルです。このような一時的なテーブルに対しては、通常Eloquentモデルは作成しません。

Eloquentモデルはabstructクラスのため、直接インスタンス化できません。どこかのクラスでextendsする必要があります。わざわざ一時的な結果に対してクラスを作成するのもなんなんで、今回はAppleモデルを流用しましょう。

$eloqunetModel = new Apple;

Appleモデルは本来、取得するために必要なテーブルの情報など色々含んでいますが、今回利用したいのはオブジェクトの中のメソッドだけです。ですから、Eloquentモデルであれば何でも良いのです。なにせ、Eloquentモデルはマッピングを持ちません。直接テーブルのフィールドを管理していませんからね。Eloqunetを継承するオブジェクトであれば、何でも使えます。

LEFT JOINの結果(stdClassの配列)をEloquentモデルの配列に変換します。一件ずつ処理します。

$models = [ ];
foreach( $results as $result )
{
    $models[] = $eloquentModel->newFromBuilder( $result );
}

これで$modelsはEloquentモデル(Apple)の配列になりました。

次に、これをコレクションクラスに変換します。

$collections = $eloquentModel->newCollection($models);

これで、先ほど紹介したCollectionクラスに含まれる操作ができるようになります。(はずです。:D 真面目に試していません。:D :D )実際にはEloquentはもっと色々やっていますが、大まかな流れはこんな感じでテーブルの取得結果を、最終的にコレクションに変換しています。

実際にテーブル操作の結果をこのように手動で変換する必要は全くありません。Eloquentの最初の説明で行ったように、JOIN結果を(理解しやすい概念的にはEloquentモデルの配列として、実際的には)コレクションとして、直接取得できます。もちろん、Eloquentモデルに対しクエリービルダーにより取得した結果は全部、Eloqunetモデルのコレクションになります。

さあ、コレクションが取得できました。後は煮るなり、焼くなり、好きに処理して下さい。皆さんの腕の見せ所です。