はじめに
本記事はLaravel #2 Advent Calendar 2019の8日目の記事です。
TL;DR
- 端的に言うと
GeneaLabs/laravel-model-caching1を用いたデータベースの結果キャッシュのススメ - データベースへのクエリ数を何とか減らしたい場合に有効
 - 追加実装することなく、クエリ結果をキャッシュしてくれる
 - 「雑にキャッシュする」とは、
GeneaLabs/laravel-model-cachingが複雑なリレーションをキャッシュしてくれないから - サンプルアプリを作ったので、とにかく手を動かして覚えたい方は、こちら2を参照のこと
 
Laravelのキャッシュといえば
Laravel公式のドキュメント3にあるように、データベースから取得した結果を特定のキーに紐づけてキャッシュする方法を思い浮かべると思います。
$value = Cache::get('key', function () {
    return DB::table(...)->get();
});
ただ、この方法では、以下の問題があります。
- 全てのデータ取得箇所に追加の実装が必要
 - ルートパラメータやバリデーション(
exists)など、フレームワークやライブラリ側でデータ取得している箇所については(簡単に)キャッシュできない 
GeneaLabs/laravel-model-cachingのススメ
私なりのオススメポイントは以下のとおり。
- インストールして、少し設定するだけで、(それなりに)動いてくれる
 - フレームワークやライブラリ側で呼び出されるクエリもキャッシュしてくれる
 
注意点
- 
GeneaLabs/laravel-model-cachingは、WHERE句やORDER BY句などの条件をキャッシュキーにする - 条件が複雑になればなるほど、キャッシュ効率(ヒット率)は下がっていく(同じ条件のクエリが続けて実行されればその限りではないものの)
 - 
Eager Loading[^Eager Loading]などによるクエリ効率化(=N+1問題解消)とGeneaLabs/laravel-model-cachingは相性が悪い- データが多いと
IN句が長くなり、結果的にキャッシュ効率が下がる - 後述の
HasOneThroughやHasManyThroughなリレーションモデルはキャッシュしないという問題がある 
 - データが多いと
 - 複雑なクエリの場合は、オーソドックスなキャッシング3を使ったほうが良さそう
 
サンプルアプリ実装上のポイント
サンプルアプリ実装上のポイントは以下のとおりです。
- DBはSQLite
 - データベースファイルは、SQLite Sample Database And Its Diagram (in PDF format)からダウンロード
 - 
albums、artists、tracksの3つのリソースについて、いくつかのAPIを実装 - キャッシュストレージは
APCu 
セットアップ方法については、imunew/laravel-model-caching-exampleのREADMEを参照のこと。
docker-compose up -dでアプリを起動し、http://localhost/api/artistsにアクセスすると、以下のようなjsonが返ってきます。
{
    "artists": [
        {
            "id": 1,
            "name": "AC/DC",
            "albums": [
                {
                    "id": 1,
                    "title": "For Those About To Rock We Salute You"
                },
                {
                    "id": 4,
                    "title": "Let There Be Rock"
                }
            ]
        },
        {"..."},
        {
            "id": 275,
            "name": "Philip Glass Ensemble",
            "albums": [
                {
                    "id": 347,
                    "title": "Koyaanisqatsi (Soundtrack from the Motion Picture)"
                }
            ]
        }
    ]
}
APC管理画面を配置
http://localhost/apc.phpにアクセスすると、下記のような管理画面に遷移します。
krakjoe/apcu: APCu - APC User Cacheのapc.phpをpublicにそのまま置きました。
キャッシュの操作などで、BASIC認証が必要となるのですが、デフォルトパスワードではログインできないので、そこだけ変更しています。
キャッシュが効いているか確認する
itsgoingd/clockworkというプロファイリングツールをインストールしているので、ブラウザで実行されたSQLなど確認できます。
Chrome(とFirefox)用のエクステンション[^Chrome Extension]があるので、インストールしておきます。
キャッシュなしの場合
事前に前述のAPC管理画面からキャッシュをクリアしておきます。
http://localhost/api/artists/1にアクセスすると、以下のようにClockworkのDatabaseタブで2つのクエリが実行されたことを確認できます。
SELECT * FROM "artists" WHERE "ArtistId" = '1' LIMIT 1
SELECT * FROM "albums" WHERE "albums"."ArtistId" = '1' 
and "albums"."ArtistId" IS not NULL
キャッシュありの場合
続けて、もう一度、http://localhost/api/artists/1にアクセスします。
すると、実行されたクエリがないため、Databaseタブが表示されません。
Cacheタブを確認すると、キャッシュエントリの件数などを確認することができます。
キャッシュの詳細
APC管理画面のUser Cache Entriesボタンからキャッシュエントリーの一覧が表示されます。
リストをクリックすると、各キャッシュエントリの詳細を確認することができます。
キャッシュされないケース
GeneaLabs/laravel-model-cachingのissues[^Github Issues]にもあるように、HasOneThroughやHasManyThroughなリレーションモデルはキャッシュしない。
試しに、http://localhost/api/artists/1/tracksに複数回アクセスしても、下記のSQLが毎回実行されます。
SELECT "tracks".*, "albums"."ArtistId" as "laravel_through_key" 
FROM "tracks" 
inner join "albums" on "albums"."AlbumId" = "tracks"."AlbumId" 
WHERE "albums"."ArtistId" = '1'
尚、Lazy LoadingでもEager Loadingでも同じくキャッシュされなかった。
おわりに
2019-12-08時点でv0.7.3とまだまだ発展途上なGeneaLabs/laravel-model-cachingですが、
今後のアップデートを注視しつつ、上手に活用していきたいと思います。
興味ある方は、サンプルアプリを使って研究してみてください。
ではでは。
- 
https://laravel.com/docs/6.x/cache
[^Eager Loading]: https://laravel.com/docs/6.x/eloquent-relationships#eager-loading
[^Chrome Extension]: https://chrome.google.com/webstore/detail/clockwork/dmggabnehkmmfmdffgajcflpdjlnoemp
[^Github Issues]: https://github.com/GeneaLabs/laravel-model-caching/issues/279#issuecomment-531597866 ↩ ↩2 




