|
| 1 | +# Cache Query |
| 2 | + |
| 3 | +Remember your query results using only one method. Yes, only one. |
| 4 | + |
| 5 | +```php |
| 6 | +Articles::latest('published_at')->cache()->take(10)->get(); |
| 7 | +``` |
| 8 | + |
| 9 | +## Requirements |
| 10 | + |
| 11 | +* PHP 8.0 |
| 12 | +* Laravel 9.x |
| 13 | + |
| 14 | +## Installation |
| 15 | + |
| 16 | +You can install the package via composer: |
| 17 | + |
| 18 | +```bash |
| 19 | +composer require laragear/cache-query |
| 20 | +``` |
| 21 | + |
| 22 | +## Usage |
| 23 | + |
| 24 | +Just use the `cache()` method to remember the results of a query for a default of 60 seconds. |
| 25 | + |
| 26 | +```php |
| 27 | +use Illuminate\Support\Facades\DB; |
| 28 | +use App\Models\Article; |
| 29 | + |
| 30 | +$database = DB::table('articles')->latest('published_at')->take(10)->cache()->get(); |
| 31 | + |
| 32 | +$eloquent = Article::latest('published_at')->take(10)->cache()->get(); |
| 33 | +``` |
| 34 | + |
| 35 | +The next time you call the **same** query, the result will be retrieved from the cache instead of running the SQL statement in the database, even if the result is empty, `null` or `false`. |
| 36 | + |
| 37 | +Since it's [eager load unaware](#eager-load-unaware), you can also cache (or not) an eager loaded relation. |
| 38 | + |
| 39 | +```php |
| 40 | +use App\Models\User; |
| 41 | + |
| 42 | +$eloquent = User::where('is_author')->with('posts' => function ($posts) { |
| 43 | + $post->cache()->where('published_at', '>', now()); |
| 44 | +})->paginate(); |
| 45 | +``` |
| 46 | + |
| 47 | +### Time-to-live |
| 48 | + |
| 49 | +By default, results of a query are cached by 60 seconds, but you're free to use any length, `Datetime`, `DateInterval` or Carbon instance. |
| 50 | + |
| 51 | +```php |
| 52 | +use Illuminate\Support\Facades\DB; |
| 53 | +use App\Models\Article; |
| 54 | + |
| 55 | +DB::table('articles')->latest('published_at')->take(10)->cache(120)->get(); |
| 56 | + |
| 57 | +Article::latest('published_at')->take(10)->cache(now()->addHour())->get(); |
| 58 | +``` |
| 59 | + |
| 60 | +### Custom Cache Key |
| 61 | + |
| 62 | +The auto-generated cache key is an BASE64-MD5 hash of the SQL query and its bindings, which avoids any collision with other queries while keeping the cache key short for a faster lookup in the cache store. |
| 63 | + |
| 64 | +```php |
| 65 | +Article::latest('published_at')->take(10)->cache(30, 'latest_articles')->get(); |
| 66 | +``` |
| 67 | + |
| 68 | +You can use this to your advantage to manually retrieve the result across your application: |
| 69 | + |
| 70 | +```php |
| 71 | +$cachedArticles = Cache::get('cache-query|latest_articles'); |
| 72 | +``` |
| 73 | + |
| 74 | +### Custom Cache Store |
| 75 | + |
| 76 | +You can use any other Cache Store different from the application default by setting a third parameter, or a named parameter. |
| 77 | + |
| 78 | +```php |
| 79 | +Article::latest('published_at')->take(10)->cache(store: 'redis')->get(); |
| 80 | +``` |
| 81 | + |
| 82 | +### Cache Lock (data races) |
| 83 | + |
| 84 | +On multiple processes, the query may be executed multiple times until the first process is able to store the result in the cache, specially when these take more than one second. To avoid this, set the `wait` parameter with the number of seconds to hold the acquired lock. |
| 85 | + |
| 86 | +```php |
| 87 | +Article::latest('published_at')->take(200)->cache(wait: 5)->get(); |
| 88 | +``` |
| 89 | + |
| 90 | +The first process will acquire the lock for the given seconds and execute the query. The next processes will wait the same amount of seconds until the first process stores the result in the cache to retrieve it. |
| 91 | + |
| 92 | +> If you need to use this across multiple processes, use the [cache lock](https://laravel.com/docs/cache#managing-locks-across-processes) directly. |
| 93 | +
|
| 94 | +### Idempotent queries |
| 95 | + |
| 96 | +While the reason behind remembering a Query is to cache the data retrieved from a database, you can use this to your advantage to create [idempotent](https://en.wikipedia.org/wiki/Idempotence) queries. |
| 97 | + |
| 98 | +For example, you can make this query only execute once every day for a given user ID. |
| 99 | + |
| 100 | +```php |
| 101 | +$key = auth()->user()->getAuthIdentifier(); |
| 102 | + |
| 103 | +Article::whereKey(54)->cache(now()->addHour(), "user:$key")->increment('unique_views'); |
| 104 | +``` |
| 105 | + |
| 106 | +Subsequent executions of this query won't be executed at all until the cache expires, so in the above example we have surprisingly created a "unique views" mechanic. |
| 107 | + |
| 108 | +## Caveats |
| 109 | + |
| 110 | +This cache package does some clever things to always retrieve the data from the cache, or populate it with the results, in an opaque way and using just one method, but this world is far from perfect. |
| 111 | + |
| 112 | +### Operations are **NOT** commutative |
| 113 | + |
| 114 | +Altering the Builder methods order will change the auto-generated cache key. Even if two or more queries are _visually_ the same, the order of statements makes the hash completely different. |
| 115 | + |
| 116 | +For example, given two similar queries in different parts of the application, these both will **not** share the same cached result: |
| 117 | + |
| 118 | +```php |
| 119 | +User::query()->cache()->whereName('Joe')->whereAge(20)->first(); |
| 120 | +// Cache key: "query-cache|/XreUO1yaZ4BzH2W6LtBSA==" |
| 121 | + |
| 122 | +User::query()->cache()->whereAge(20)->whereName('Joe')->first(); |
| 123 | +// Cache key: "query-cache|muDJevbVppCsTFcdeZBxsA==" |
| 124 | +``` |
| 125 | + |
| 126 | +To ensure you're hitting the same cache on similar queries, use a [custom cache key](#custom-cache-key). With this, all queries using the same key will share the same cached result: |
| 127 | + |
| 128 | +```php |
| 129 | +User::query()->cache(60, 'find_joe')->whereName('Joe')->whereAge(20)->first(); |
| 130 | +User::query()->cache(60, 'find_joe')->whereAge(20)->whereName('Joe')->first(); |
| 131 | +``` |
| 132 | + |
| 133 | +### Eager load **unaware** |
| 134 | + |
| 135 | +Since caching only works for the current query builder instance, an underlying Eager Load query won't be cached. |
| 136 | + |
| 137 | +```php |
| 138 | +$page = 1; |
| 139 | + |
| 140 | +User::with('posts', function ($posts) use ($page) { |
| 141 | + return $posts()->forPage($page); |
| 142 | +})->cache()->find(1); |
| 143 | +``` |
| 144 | + |
| 145 | +In the example, the `posts` eager load query results are never cached. To avoid that, you can use `cache()` on the eager loaded query. This way both the parent `user` query and the child `posts` query will be saved into the cache. |
| 146 | + |
| 147 | +```php |
| 148 | +$page = 1; |
| 149 | + |
| 150 | +User::with('posts', function ($posts) use ($page) { |
| 151 | + return $posts()->cache()->forPage($page); |
| 152 | +})->find(1); |
| 153 | +``` |
| 154 | + |
| 155 | +## How it works? |
| 156 | + |
| 157 | +When you use `cache()`, it will wrap the base builder into a `CacheAwareProxy` proxy calls to it. At the same time, it injects a callback that runs _before_ is sent to the database for execution. |
| 158 | + |
| 159 | +This callback checks if the results are in the cache. On cache hit, it throws an exception to interrupt the query, which is recovered by the `CacheAwareProxy`, returning the results. |
| 160 | + |
| 161 | +For the Eloquent Builder, this wraps happens below it, so all calls pass through the `CacheAwareProxy` before hitting the real base builder. |
| 162 | + |
| 163 | +## Security |
| 164 | + |
| 165 | +If you discover any security related issues, please email darkghosthunter@gmail.com instead of using the issue tracker. |
0 commit comments