書籍の管理と貸し出し申請をウェブ上で行うことができるアプリケーション「BookMark」。読み終わった本を返却しようとしたその時、本は返却済み扱いにはならずエラー画面に遷移してしまいました。
バグの原因は返却対象の書籍情報を取得するクエリビルダにありました。今回はこのクエリビルダがどんなSQLを実行しているのか見てみたいと思います。
■結論
主キーで取得するならfind()を。
それ以外ならwhere()&first()を使おう。
■該当のソースコード
本の貸出状態は、booksテーブルの「user_id」カラムにユーザーIDがあるかないかで行っています。そのため、返却処理は「STEP1.リクエスト内の$bookIdからBookモデルを取得」「STEP2.user_idをnullで更新」で行っていました。
public function destroy(Request $request)
{
$user = Auth::user();
$bookId = $request->application;
// booksテーブルの所有者をnullで更新
$book = Book::find($bookId)->first();
$book->user_id = null;
$book->save();
//(以下略)
問題は$book = Book::find($bookId)->first();
の部分。find()とfirst()を一つのクエリで行っています。
■tinkerでどうなっているのか確認
tinkerとは
tinkerとはLaravelでプログラムを対話的に扱える、ちょっとした動作確認に使える便利なシステムです。
引用:https://qiita.com/yu_eguchi/items/419a2a081ff142ecf67f
tinkerを起動して、書籍ID=2を取得してみます。
// tinker起動
bash-4.2# php artisan tinker
Psy Shell v0.9.12 (PHP 7.3.11 — cli) by Justin Hileman
// Bookモデルでid=2のレコードを取得
>>> use App\Book;
>>> Book::find(2);
=> App\Book {#3056
id: 2,
book_name: "JUnit実践入門",
user_id: null,
created_at: "2020-01-16 06:51:25",
updated_at: "2020-02-02 13:45:07",
title_kana: "ジェイユニット ジッセン ニュウモン",
subtitle: "体系的に学ぶユニットテストの技法",
subtitle_kana: "タイケイテキ ニ マナブ ユニット テスト ノ ギホウ",
isbn: "9784774153773",
author: "渡辺修司",
author_kana: "ワタナベ,シュウジ",
publisher: "技術評論社",
url: "https://books.rakuten.co.jp/rb/12058050/",
}
きちんとID=2が取得できています。
次は、これにfirst()をつけてみます。正常な動作であれば同じ結果になるはずです。
// これにfirst()をつけてみる。
>>> >>> Book::find(2)->first();
=> App\Book {#3047
id: 1,
book_name: "SOFT SKILLS",
user_id: null,
created_at: "2020-01-16 06:51:25",
updated_at: "2020-02-02 13:45:23",
title_kana: "ソフト スキルズ",
subtitle: "ソフトウェア開発者の人生マニュアル",
subtitle_kana: "ソフトウェア カイハツシャ ノ ジンセイ マニュアル",
isbn: "9784822251550",
author: "ジョン・Z.ソンメズ/長尾高弘",
author_kana: "ソンメズ,ジョン・Z./ナガオ,タカヒロ",
publisher: "日経BP社",
url: "https://books.rakuten.co.jp/rb/14141677/",
}
結果が変わりました。どうやら、おかしいのはこの部分のようです。
■実行クエリを確認
実際に実行されているクエリが見たいですね。そんなときは、enableQueryLog()
と getQueryLog()
を使いましょう。
\DB::enableQueryLog();
// 見たいクエリ
dd(\DB::getQueryLog());
これで確認ができます。
public function destroy(Request $request)
{
$user = Auth::user();
$bookId = $request->application;<img width="388" alt="dd_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250884/bfffecdf-e6e7-5063-a6ef-38777036f7d2.png">
// 見たいクエリの前に記述。
\DB::enableQueryLog();
$book = Book::find($bookId)->first();
// dd()で実行されたクエリを見る。
dd(\DB::getQueryLog());
$book->user_id = null;
$book->save();
//(以下略)
原因が判明しましたね。
実行したいクエリselect * from books where books.id=1 limig 1;
の後に、select * from books limit 1;
が実行されていたため、想定の結果とは異なるデータになっていたようです。
では、$book = Book::find($bookId);
のクエリはどうなるのでしょうか。
まとめるとこのような実行内容になるようです。
// findでidを指定
Book::find(2);
→ select * from books where id = 2 limit 1;
// whereでidを指定した後、first()で1レコード取得
Book::where('id', 2)->first();
→ select * from books where id = 2 limit 1;
■まとめ:主キーならfind()。それ以外ならwhere()&first()。
今回のバグは、find()とwhere()&first()の理解不足だったため起きました。
find()で指定するのは主キーだからfirst()を使わなくても1レコードに特定することができるという点に気づけばこのような勘違いは起きなかったと思います。以後、気を付けます。
それでは!