はじめに
はじめまして、オープンロジのエンジニアで @ttaka と申します。
この記事は OPENLOGI Advent Calendar 2017 の8日目になります。
さて、弊社サービスのサーバーサイド開発には、 PHP のフレームワークである Laravel が使われています。今回はその Laravel の ORM である Eloquent を使う際に、ちょっとハマりそうなポイントをいくつかご紹介したいと思います。
ハマりポイント
論理削除トレイトはテーブルの結合先では動作しない
論理削除自体には賛否ありますが、 Eloquent には標準でその機能が提供されています。
例えば、次のような論理削除を使用している User
と Profile
クラスがあるとします。
class User extends Model
{
use SoftDeletes;
public function profile()
{
return $this->hasOne(Profile::class);
}
}
class Profile extends Model
{
use SoftDeletes;
}
各モデルで find
や where
などを利用すると、論理削除されたレコードを除外する条件が自動的にクエリーへ追加されます。
User::where('name', 'hoge')
->toSql();
select * from `users`
where `name` = ?
and `users`.`deleted_at` is null
また、定義したリレーションを使って Profile
を取得した場合や、 with
を使った Eager Loading の場合も、論理削除されたレコードは自動的に除外されます。
$user = User::find(1);
$profile = $user->profile; // 論理削除されてると null が返る
$user = User::with('profile')->find(1);
$profile = $user->profile; // 論理削除されてると null が返る
では、次のように Profile
が存在する User
のみ取得しようと join
を指定した場合、結合先の条件はどうなるでしょうか?
User::join('profiles', 'users.id', '=', 'profiles.user_id')
->toSql();
select * from `users`
inner join `profiles` on `users`.`id` = `profiles`.`user_id`
where `users`.`deleted_at` is null
このように、結合先の profiles
に対しては論理削除の条件が付加されません。そのため、手動で profiles.deleted_at is null
の条件を加える必要があります。
クエリービルダーによる削除ではオブザーバーが動作しない
Eloquent には Observer を設定することで、作成や削除といったイベント時に特定の処理をさせる機能があります。
ただ、クエリービルダーによる delete
などでは、そのイベントが発火しません。
$user->items()->delete(); // 発火しない
そのため、ちょっと面倒ではありますが、各 Eloquent インスタンスの delete
メソッドを呼んであげる必要があります。
$user->items->each(function ($item) {
$item->delete(); // 発火する
});
テーブルを結合すると同名のカラムは上書きされる
join
先のテーブルに同じ名称のカラムがあると、主テーブルの値が上書きされてしまうため、必ず select
を一緒に指定するようにしましょう。
User::join('profiles', 'users.id', '=', 'profiles.user_id')
->select('users.*') // 必ず指定しよう!
->get();
予約語のカラム名を使う際は取得時に注意
hidden
などのように Eloquent 側で利用しているカラム名を使うと、そのクラスのメソッド内から $this->hidden
ではアクセスできません。その際は $this->getAttribute('hidden')
のようにアクセスする必要があります。
条件に OR を使う際は必ずグループ化する
一見すると問題なさそうな次のクエリーですが……
User::where('name', 'hoge')
->orWhere('is_admin', true)
->get();
論理削除を使ってると、自動的に条件が追加されて正しく動作しなくなります。そのため、必ずグルーピングしてまとめる必要があります。
User::where(function ($query) {
$query->where('name', 'hoge')
->orWhere('is_admin', true);
})->get();
論理削除などを使ってなくても、後から条件を追加することはありますし、常にグループ化しておきたいですね。
chunk を使う際は必ず orderBy を指定
レコードが大量にあっても、分割して取得できる便利な chunk
メソッドがあります。
Users::query()->chunk(100, function ($users) {
foreach ($users as $user) {
// 悪い例
}
});
このメソッドですが、内部的には SELECT * FROM users LIMIT ?,?
の結果を順に返してるだけですので、次のように orderBy
を指定しないと、正しく動作しないことがあります。
Users::query()->orderBy('id')->chunk(100, function ($users) {
foreach ($users as $user) {
// 良い例
}
});
SQL では ORDER
を付け忘れないのに、ソースコードになると見た目の問題かつい忘れがちなので、気をつけたいですね。
ちなみに Laravel のドキュメントでも、ちゃんと orderBy
を指定した例になってます。
属性キャストを指定しないとそのままの値
例えば、マイグレーションで is_admin
カラムを次のように指定し、レコードには 0/1
の値が格納されているものとします。
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false);
});
このまま $user->is_admin
にアクセスしても、残念ながらそのまま 0/1
の値が返ってきます……。
そんな時は、属性キャストを指定することで、その型に自動変換されます。
class User extends Model
{
protected $casts = [
'is_admin' => 'boolean',
];
}
これで曖昧比較が嫌いな方も、安心して $user->is_admin === true
で判定できますね。
おわりに
以上、知ってれば何ということはないものですが、他のフレームワークなどと動きが違っていたり、ついやってしまいがちなことをいくつかまとめてみました。
アドベントカレンダーどころか Qiita に書くのもはじめてのことなので、何か至らないところがありましたら、ご指摘くださると助かります。
それでは、みなさんが Eloquent を使って何かハマったときに、解決の一助になれば幸いです。