Eloquentでクエリする際も内部的にはクエリビルダのメソッドをcallしている事が多いので、何も考えずにクエリビルダで出来ていた事をそのままEloquentで実行していたらハマってしまいました、落とし穴に。おかげで「なるほど、そういう事もあるのだな」位の心構えができるようになりました。今回はそんな気づきを与えてくれたvalue()についての解説です。

value()とは?

value()はクエリの最初の結果から特定の項目のみ取得して返してくれる便利なメソッドです。DBからfetchするレコードが1つだと分かっていて、且つ、特定の1項目のみが必要な場合などに重宝します。

例えば、ユーザの名前と生年月日の組み合わせがユニークであり、そのemailを取得したい場合は

$email = DB::table('user')
    ->where('name', '=', '山田太郎')
    ->where('birthday', '=', '1970-01-01')
    ->value('email');
>>> "y.taro@sample.com"

と書けば一発で取得できます。
上記はクエリビルダでの取得ですが、Eloquentでも同様にvalue()を使うことが出来ます。

$email = User::query()
    ->where('name', '=', '山田太郎')
    ->where('birthday', '=', '1970-01-01')
    ->value('email');
>>> "y.taro@sample.com"

いずれも実行されるsqlは以下の通り、

select `email` from `member` where `name` = '山田太郎' and `birthday` = '1970-01-01' limit 1;

特定の項目のみselectしつつ、limitで取得する結果を1つに限定している点がポイントです。

こうしてみると、クエリビルダとEloquentのvalue()は一見同じ処理を行っているように見えますね。
しかし、次の例では異なる結果を返します。

私がハマった落とし穴

以下ではとあるブログの投稿(post)に紐付いたタグ名(tag.name)を取得しGROUP_CONCATで連結して取得しました。

$tags = DB::table('post')
    ->join('tag', 'post.post_id', '=', 'tag.post_id')
    ->where('post.post_id', '=', 1)
    ->groupBy('post.post_id')
    ->value(DB::raw('GROUP_CONCAT(tag.name)'));
>> "Laravel,PHP,Eloquent"

クエリビルダではカンマで連結されたタグ名が取得できました。
Eloquentではどうでしょうか?

$tags = Post::query()
    ->join('tag', 'post.post_id', '=', 'tag.post_id')
    ->where('post.post_id', '=', 1)
    ->groupBy('post.post_id')
    ->value(DB::raw('GROUP_CONCAT(tag.name)'));
>> null

あれれ、どういう訳かnullが返されました。

ソースを解析

以下はEloquentのvalue()のソースコードです。

/**
 * Get a single column's value from the first result of a query.
 *
 * @param  string|\Illuminate\Database\Query\Expression  $column
 * @return mixed
 */
public function value($column)
{
    if ($result = $this->first([$column])) {
        return $result->{Str::afterLast($column, '.')};
    }
}

なるほど、first([$column])でクエリ結果を取得し、$columnで指定した値のコロン(’.’)以降をプロパティ名としてアクセスしています。

つまり先ほどの例だと、$columnはGROUP_CONCAT(tag.name)なので、コロン以降はname)となります。従って、Undefined propertyとして処理され、nullとなった訳です。

一方、結果が正しく取得できたクエリビルダはというと、

/**
 * Get a single column's value from the first result of a query.
 *
 * @param  string  $column
 * @return mixed
 */
public function value($column)
{
    $result = (array) $this->first([$column]);

    return count($result) > 0 ? reset($result) : null;
}

配列に変換した上で、reset()を使って1つ目の結果を取得していました。

回避策

回避策は色々ありそうですが、私の中では生クエリ(DB::raw)とvalue()を併用したい場合は一度selectRaw()などで別名を付けるルールを採用する事にしました。

$tags = Post::query()
    ->join('tag', 'post.post_id', '=', 'tag.post_id')
    ->where('post.post_id', '=', 1)
    ->groupBy('post.post_id')
    ->selectRaw('GROUP_CONCAT(tag.name) AS tag_names')
    ->value('tag_names');
>> "Laravel,PHP,Eloquent"

現場からは以上です。

By hikaru