速習Larave3(その13)、コメントNo.3

タグ: Laravel3  

前回に引き続き、Eloquent ORMの解説を行います。

Eloquent ORMの操作

まずレコード追加から見てみましょう。コメントコントローラーをご覧ください。(レコード追加以外は既に学んだものばかりです。)

//            $comment = new Comment(array(
//                'user_id' => Auth::user()->id,
//                'post_id' => $id,
//                'comment' => $inputs['comment'],
//            ));
//            $comment->save();
            $comment = new Comment(array(
                'user_id' => Auth::user()->id,
                'comment' => $inputs['comment'],
            ));
            $post->comments()->insert($comment);

コメントアウトしているところが今まで学んだコメントの追加のおこない方です。これも正しく動作します。関連づけのuser_idpost_idを自前で両方共セットしています。これは「コメントを追加する」基本的な方法です。

その下、実際のコードをご覧ください。コメントあるとしているコードと比べると、生成するCommentモデルのインスタンスで、記事のidをセットしていません。その代わり記事($post)の関連付けを利用してコメントを挿入しています。この方法で、post_idは自動的にセットされます。

たいして便利でないでないなという声が聞こえてきます。ええ、やっていることはひと手間減らしているだけです。しかしよく見てみましょう。

$post->comments()->insert($comment);は「記事に、新しいコメントを挿入する」と読めませんか?コメントアウトしている書き方と比べて、コーディングしている人の意図が読み取れます。そうです。可読性が上がっています。

続いてコメントの取得を見てみましょう。表示するためにPostコントローラーの記事表示で取得しています。

$post = Post::with(array('comments', 'comments.user'))->find($id);

なんだか単純なんだか、複雑なんだかわかりませんね。これは応用型です。基本から見て行きましょう。

単純に一つの記事を取得するには:

$post = Post::find($post_id);

これで$postにはidに$post_idを持つ、Postクラスのインスタンスが受け取れます。

今回、関係づけを行いました。ですから記事とコメントを取得するにはこう書きます。

$post = Post::find($post_id);
$comments = $post->comments()->get();

$postは記事クラスのインスタンスです。$post->idにはその記事のidがあります。$postを通じてcomment()メソッド=関連付けでデータを取得(get)します。つまりコメントテーブルのpost_idが$post->idの値のレコードをwhereしてくれるわけです。

2行目は別の書き方もできます。

$comments = $post->comments;

もちろん、記事情報は必要なく、コメントだけが欲しい場合は続けて記述できます。

$comments = Post::find($post_id)->comments;

では、両方欲しい場合はどうしましょう?別々に書く書き方は見て見ましたね。

今回の場合でしたら、一つの記事とそれに対するコメントを一度に取得する方法が、Eagerローディングです。実際に書いてみます。

$post = Post::with('comments')->find($post_id);

この$postをvar_dump()もしくは、Laravelのdd()ヘルパー、デバッガーなどで内容をご覧いただくと分かるのですが、記事のインスタンスにcommentsというリレーションのプロパティーにCommetモデルが追加されています。

$postからEagerローディングを使わない場合と同様に、記事の情報は取り出せます。

echo $post->id; // id
echo $post->title; // タイトル
echo $post->body; // 本文

しかも、この記事に対する情報も取り出せます。コメントは複数の可能性があるため、インスタンスの配列です。

foreach($post->comments as $comment)
{
    $comment->comment; // コメントNo
    $commmet->user_id; // ユーザーid
}

既にコメントは記事の情報と一緒にインスタンス化されています。メモリ上にあります。ですから、その分メモリは食っていますが、余計なDBアクセスは発生しません。

とはいえ、ユーザーidをそのままエコーしたのでは、ユーザーにはわかりづらいですね。やはりユーザー名を表示したいものです。

foreach($post->comments as $comment)
{
    $comment->comment; // コメントNo
    $user = User::find($commmet->user_id);
    echo $user->username; // ユーザー名
}

これでOK…なんですが、ループの中でUser::find()を使用しています。このコードではコメント数だけテーブルへのアクセスが発生してしまいます。これでは速度が遅くなります。

これこそEagerロードが生まれた背景です。Laravelの発明ではありません。(ちなみに、Eagerロードを使わないこの方法は特に名前はありません。使わないこの方法をLazyロードと書かれている記事もありますが、Lazyロードは別物です。Lazyロードとは大きな情報をテーブルの読み込み時に他のカラムと一緒に読み込んでしまうと、多くのレコードを取得した場合にメモリを圧迫するため、実際に使用する段階まで取り込みを遅らせる仕組みのことです。Laravelには用意されていません。)

では、Eagerロードでusersテーブルの情報まで一度に取り込みましょう。それが、最初のコードに記述した書き方です。

$post = Post::with(array('comments', 'comments.user'))->find($id);

配列で指定します。最初のcommentsでコメントを取り込みます。そのコメントにはuser()の関係付けがありましたね。それを利用し、今取り込んだcommentsを書いたユーザー情報も取り込んでねという書き方です。

$post = Post::with(array('comments', 'comments.user'))->find($id);
echo $post->title; // 記事の情報にアクセスできます。
foreach($post->comments as $comment)
{
    echo $comment->comment; // コメントの情報にもアクセスできます。
    echo $comment->user->username; // ユーザーの情報にもアクセスできます。
}

既に記事インスタンスに取り込んだ内容、つまりメモリ上に存在する情報にアクセスしているだけですので、データベースアクセスはコメントの数だけ発生することはありません。

ビューで実際に上記の方法で、情報にアクセスし、表示しています。

Eloquentの使用上の注意

ドキュメントに明記されていますが、Eagerローディングの際、生成されるコードはWHERE ... IN ...を使用します。

SELECT * FROM "commnets" WHERE "id" IN (1, 2, 3, 4, 5, ...)

これはレコードの多くなる項目に対してEagerローディングを使用すると、SQL文が長くなりすぎ、エラーが起きる可能性があることを示唆しています。実際。SQLサーバーを使用したユーザーがその制限に引っかかりフォーラムで質問していました。

EagerローディングではJOINを生成するORMが多いらしく、以前に他のORMを期待していた方の中にはJOINで生成されないことに失望される方もいらっしゃるようです。それは仕様ですから仕方がありません。ORMがSQLのラッパーの性質を持っている限り、利点も欠点も、どんなコードが生成されるかも、頭の片隅に置いて使用すれば良いわけです。いずれにせよ、必要な情報を全部メモリなどに取得する限り、大きな結果を得ようとすれば、何かの資源を圧迫します。それはJOINでもWHERE INでも変わりません。もし、特定のSQLがお望みであれば、その部分は直接SQLで書いてもいいですし、Fluentクエリービルダーを使用しても良いわけです。全部をEloquentで記述することに固執する必要はありません。

基本的なことですが、開発環境ではメモリをふんだんに使用できても、実機環境では制限されていることがあります。Eloqunetを使用する、しないにかかわらず、大きなデータの取得時には、サイズが大きくなり過ぎないよう、分割して読み込むなど、予め対策が必要です。実機で動かないパターンの原因の一つです。

なぜこんなことを書くかって?それはもちろんフォーラムにこの手の質問がたまに上がるからです。;) 特にPaaS使用者は制限されていますからね。お金払えば別ですが、皆んな「無料」でできる範囲内でやりくりしようと苦労しているのです。