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テーブルです。更新日時このテーブルを全件取得するには次のように書きます。DB::table('apples`)->get();table()でテーブル名を指定します。get()でSELECT実行です。条件を付けたければwhere()メソッドなど豊富に用意されています。getで実行するまで、クエリーは実行されません。メソッドを使って動的に組み立てることも可能です。文字列操作でSQL文を生成するよりも、クエリービルダーのメソッドを利用したほうが、わかりやすく書けます。これは、クエリービルダーを使う利点の一つです。結果の$resultsはstdClassの配列です。このSELECTの結果はPDOの定数で指定したものです。app/config/database.phpの最初の設定オプションです。PDO::FETCH_CLASS,PDOの取得オブジェクトの指定は、http://www.php.net/manual/ja/pdo.constants.phpに記述されている多くの取得方法が利用できます。使い所が難しい物もありますね。配列の要素がstdClassなのが気に入らない、配列のほうが良い人という人ならば、PDO::FETCH_ASSOCなどへ指定を変更できます。後ほど紹介する、ORMのEloquentも併用したい人は、デフォルトのままにしておきましょう。りんごテーブルは、いわゆる典型的な「マスター」テーブルです。コード、この場合はIDですが、そのコードに対する情報を保存しています。マスターに対する、「トランザクション」テーブルも考えましょう。りんごの注文テーブルです。apple_ordersテーブルです。今回は作成日時を受注日時として考えましょう。更新日時こちらも全レコードを取得するには、条件を付けずにそのままget()します。DB::table('apple_orders')->get();$resultsの結果も、上記のテーブル構造をそのままstdClassのプロパティーにし、全件分レコードが配列になります。りんごマスターがあるのですから、やっぱり数字のりんごIDでなくて、人間が読めるりんご名をレコードに含みたいですよね。JOINでマスターを引っ張ってきましょう。レコードに名前を含めましょう。->get();クエリービルダーでSELECTする項目名に別名を付けたいときは、"AS"を含めて項目を指定します。(普段使わないので、ずっと忘れていました。別名をつける方法が見つからずに苦労している人は、結構いると思います。別名'とSQLそのままで指定します。SQLのSELECT文と同様に、結果に含めるフィールドを指定することも可能です。全フィールドを表す'*'も利用できます。今回はJOINするテーブル両方に同じ名前の項目(作成と更新時の日時フィールド)があるため、SELECT結果のcreated_atとupdate_atの結果は、JOINする方のapplesテーブルの内容になってしまいます。今回は手抜きで、注文テーブルの作成日時(created_at)を注文日時として取り扱うことにします。このフィールドが消えてしまうと困るので、別名(ordered_at)を付けています。取得結果には、りんご名も含まれています。後はforeachなり、array_walk関数なりで、処理してください。ORMEloquentです。Eloquentは「雄弁」という英単語です。話術に長けていることです。このORMに名付けられていなければ、一生覚えることはないような単語です。読みやすさからつけた名前でしょう。ORMはコンピューター用語ですね。mappingの略です。Wikidediaによるとオブジェクト関係マッピングだそうです。いつも説明が長い私が言うのですから、大したものです。簡単に言えば、テーブルのレコードと、プログラム言語の扱うオブジェクトを対応付けることです。Eloquentはアクティブレコードのパターンを利用していると説明されていますが、これは1レコードを1オブジェクトに対応させることです。ORMのオブジェクト、通称Eloquentモデルをご覧ください。applesテーブルに対応する、Appleモデルです。}}Eloquentモデル名は単数形のクラス名、それに対応するテーブルは複数形というデフォルトの規約があります。ORMモデルのオブジェクトが、テーブル上の1レコードを表しますので、モデル名は英語なら単数形です。それに対応するテーブルは複数レコード存在するのが通常ですので、複数形にします。これは規約というより、多くの英語を話す人たちにとって自然な命名規則です。この規約から外れる名前をつけることも可能です。ORMというとテーブルのフィールドと、クラスのプロパティーを結びつけるコーディング、正にマッピングを記述するものが多いですね。私は、それが嫌でORMは使ってきませんでした。触っては、止めてを繰り返していました。LaravelのEloquentで初めて本格的に使用し始めました。理由は、直接マッピングしなくていいからです。テーブル直したら、クラスも合わせて直す必要があるのは二度手間です。確かに、プログラミンではよくこうした手間はつきものですね。スキーマの定義とマッピングの定義で、2箇所に別れてしまうという感覚が嫌なのでした。Eloquentでは、そうしたフィールドのマッピングは必要ありません。読み込んだレコードの構造がそのまま、モデルのプロパティーとしてアクセスできます。連想配列としてもアクセスできます。悪く言えば、PDOで取り込んだ結果とさほど違いません。この単純であることが、Eloquentを使いやすくしています。ORMではモデルが含むフィールドを管理しません。取得結果に含まれているフィールドがそのままプロパティー、もしくは連想配列のキーとして指定しアクセスできます。逆にテーブルに含まれないフィールド名のプロパティー、もしくは連想配列のキーを指定することも可能です。テーブル上にフィールドが存在しないのでsaveメソッドで書き戻しても、もちろん保存されることはありません。存在しないフィールドを保存しようとするので、SQLのエラーになります。$guardedというプロパティを定義してありますが、わからない方は今回無視して下さい。マッピングではありません。その下は、リレーションの定義です。他のテーブルとの関係を定義しています。りんごテーブルの1レコードは、りんご一種類を表現しています。同じりんごに対して注文は複数件あることでしょう。テーブルのレコードを人だと思って下さい。関連を所有で表します。すると、りんごテーブルは、いくつかの注文を持っていることになります。(もちろん、不人気な商品での注文0の時もあるでしょう。1回だけということもあるでしょう。この場合の「複数」は1回、0回を含むものと考えましょう。many)」を表しています。AppleOrderモデルとは、もちろんapple_ordersテーブルのレコード1件を表す、Eloquentモデルのことです。}}Appleモデルとよく似ています。何もマッピングしていません。違いは関連付けが逆になっていることで、$this->belongsTo('Apple')です。belongsToは「〜に所属する」です。りんごモデルはりんご注文を所有していますが、りんご注文モデル1件から見れば、あるりんごモデル1件に所属しています。実際の動作で考えれば、何を意味しているのかもっとわかりやすくなります。ある一つのAppleモデルでappleOrders()メソッドが呼ばれたら、それはAppleOrderモデルが表すapple_ordersテーブルのapple_idと、Appleモデルのidフィールドの値が一致するレコード(複数)を表します。逆にある一つのAppleOrderモデルでapple()メソッドが呼び出されたら、自身のapple_idと一致するappplesテーブルのレコード(一件)を意味します。以下のコードで感覚を掴んで下さい。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を持つ全注文の取得もちろんやり方はいろいろあります。最後の問題はもちろん、注文モデルを直接取得するのではなく、りんごモデルを起点にし、関連で取得して下さい。Apple::find(1)->appleOrders;さて、Eloqunetには直接クエリービルダーが記述できます。JOINをEloquentで行ってみましょう。]);このようにEloquentモデルにクエリービルダーを記述する場合、Eloquentモデルは、単にメインの操作対象テーブルを表すだけだと考えましょう。この注文全件のレコードには、Appleテーブルから引かれたnameも、別名を付けた注文日付のordered_at含まれています。selectメソッドは使用していませんが、実はgetメソッドでも項目の選択もできます。この結果をデバッガーやvar_dumpでみてもらうとわかりますが、結果はAppleOrderクラスのオブジェクトになっています。この節の最初に説明した通り、EloqunetはSELECTの結果に基づいてフィールドを扱うだけなのです。元のテーブルに何が含まれているかどうかなど、気にかけません。私達使用者も、$ordersがどんなクラスのインスタンスの配列なのかには関心はありません。要は結果に希望のフィールドの取得結果が存在していれば、それで良いわけです。必要に応じて、Eloqunetクラスが提供するメソッドや、コレクションとしての操作ができれば満足です。クラスの名前がりんごでもみかんでもバナナでもそんなことはどうでも良いのです。何クラスに関心がないのは取得時です。更新や追加するなら、関心を持たないとダメですけどね。取得時は名前が何であれ必要なフィールドが存在していれば、みなさんも満足できるでしょう。逆に、メモリを節約するためにモデルオブジェクトに含めるフィールドを制限することもできます。りんご全件のIDと名前のみ持つAppleモデルの配列を取得するには、次のようにします。'name']);これでりんごモデルなのですが、テーブルには存在している更新と変更の日時項目を持っていないAppleモデルの配列が取得できます。$apples[1]->created_atなどのように、日時項目へアクセスすると、nullが返ってきます。Eloqunetモデルから存在しないフィールドの値を取得しようとするとnullになります。初めの頃はフィールド名を間違えた場合などで、はまりがちです。通常、オブジェクト上に存在しないプロパティーにアクセスすると、エラーになりますが、Eloquentではnullが返ります。Eloquentモデルへクエリービルダーも記述できるのですが、やはりこれはあまりORMっぽくありません。SQLの香りがプンプンします。PHPのオブジェクト操作というよりは、SQLのテーブル操作そのものです。テーブル結合はテーブルの結合です。オブジェクトの結合ではありません。Eloquentの機能を活かすには、Eagerロードを使用します。Eagerとは切望する、熱望するという意味です。Eagerロードは、ORMの用語として定着しています。AppleOrder::with('apple')->get();withメソッドに関連付けのメソッド名を指定します。すると、その関連付けからAppleレコードを取得し、結果のオブジェクトに含めます。AppleOrderオブジェクト全件分の配列と、それに対応するAppleオブジェクトの配列を結果の$ordersが持っているとイメージして下さい。Eagerロードの結果に含まれるのは、デフォルトで元のレコードに関連あるレコードだけです。今回の場合、AppleOrderに含まれているのが、りんごテーブルのIDで1,2,3のものであれば、含まれるAppleオブジェクトは1、2,3のレコードです。それ以外のレコードは含まれません。IN構文を利用します。必要なIDを列記します。SQLはエンジンの制限や設定でSQL分の長さが決まっています。これを超えないようにしましょう。apple_ordersテーブルにもともと含まれているフィールドには、直接プロパティーとしてアクセするように記述します。applesテーブルに含まれているフィールドへアクセスするには、appleというAppleOrderモデルで定義したリレーション(関連付け)を表すメソッド名を経由します。JOINを使用したときは、フィールド名がバッティングしたため消えてしまうcreate_atフィールドを別名として指定しましたが、EloquentのEagerロードでは、applesテーブルのフィールドはそのまま含まれます。Appleモデルのコレクションとして内部に含まれているからです。そのため、名前を変える必要もありません。Eloquentで取得した結果は、Laravelのコレクションです。ドキュメントに明示してありますね。実際にどんなメソッドが使えるのかを確認するためにはCollectionクラス(vendor/laravel/framework/src/Illuminate/Support/Collection.php)を確認するのが一番です。テーブルの取得結果からEloquntモデルへの変換テーブル取得結果、stdClassの配列を、Eloquentモデルに変換するために、Laravelは何を行っているのでしょうか。ポイント抜き出してみましょう。JOIN文を再度ご覧ください。->get();select()の内容をget()に移動すれば、SELECT文一文省略できるのは、もう理解できたかと思います。$resultはstdClassの配列です。JOINの結果ですから、マスターテーブルやトランザクションテーブルのような恒久的な保存テーブルではありません。一時的なテーブルです。このような一時的なテーブルに対しては、通常Eloquentモデルは作成しません。Eloquentモデルはabstructクラスのため、直接インスタンス化できません。どこかのクラスでextendsする必要があります。一時的な結果に対してクラスを作成するのもなんなんで、今回はAppleモデルを流用しましょう。Apple;Appleモデルは本来、取得するために必要なテーブルの情報など色々含んでいますが、今回利用したいのはオブジェクトの中のメソッドだけです。Eloquentモデルであれば何でも良いのです。Eloquentモデルはマッピングを持ちません。直接テーブルのフィールドを管理していませんからね。Eloqunetを継承するオブジェクトであれば、何でも使えます。JOINの結果(stdClassの配列)をEloquentモデルの配列に変換します。);}これで$modelsはEloquentモデル(Apple)の配列になりました。次に、これをコレクションクラスに変換します。$eloquentModel->newCollection($models);これで、先ほど紹介したCollectionクラスに含まれる操作ができるようになります。真面目に試していません。)実際にはEloquentはもっと色々やっていますが、大まかな流れはこんな感じでテーブルの取得結果を、最終的にコレクションに変換しています。実際にテーブル操作の結果をこのように手動で変換する必要は全くありません。Eloquentの最初の説明で行ったように、JOIN結果を(理解しやすい概念的にはEloquentモデルの配列として、実際的には)コレクションとして、直接取得できます。Eloquentモデルに対しクエリービルダーにより取得した結果は全部、Eloqunetモデルのコレクションになります。さあ、コレクションが取得できました。後は煮るなり、焼くなり、好きに処理して下さい。皆さんの腕の見せ所です。