From 5e4b961151d1cc06ad68a2d07022e05355b053d5 Mon Sep 17 00:00:00 2001 From: Alex Renoki Date: Mon, 27 Apr 2020 18:03:02 +0300 Subject: [PATCH 1/3] Added automatic cache invalidation --- README.md | 74 +++++++++++++++ database/factories/PageFactory.php | 19 ++++ src/FlushQueryCacheObserver.php | 83 ++++++++++++++++ src/Traits/QueryCacheable.php | 40 ++++++++ tests/FlushCacheOnUpdateTest.php | 94 +++++++++++++++++++ tests/Models/Page.php | 26 +++++ tests/TestCase.php | 1 + .../migrations/2018_07_14_183253_pages.php | 32 +++++++ 8 files changed, 369 insertions(+) create mode 100644 database/factories/PageFactory.php create mode 100644 src/FlushQueryCacheObserver.php create mode 100644 tests/FlushCacheOnUpdateTest.php create mode 100644 tests/Models/Page.php create mode 100644 tests/database/migrations/2018_07_14_183253_pages.php diff --git a/README.md b/README.md index 5f91219..598fcf4 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,80 @@ $user = User::with(['orders' => function ($query) { $orders = $user->orders; ``` +## Full Automatic Invalidation + +To speed up the scaffolding of invalidation within your app, you can specify the model to auto-flush the cache upon any model gets created, updated or deleted. + +```php +class Page extends Model +{ + use QueryCacheable; + + /** + * Invalidate the cache automatically + * upon update in the database. + */ + protected static $flushCacheOnUpdate = true; +} +``` + +When you set up the `$flushCacheOnUpdate` variable, the package attaches an observer to your model, and any `created`, `updated`, `deleted`, `forceDeleted` or `restored` event will trigger the cache invalidation. + +> In order for auto-flush to work, you will need at least one **base tag**. Out-of-the-box, the model has a base tag set. In some cases, if you have overwritten the `getCacheBaseTags()` with an empty array, it might not work. + +## Partial Automatic Invalidation + +In some cases, you might not want to invalidate the whole cache of a specific model. Perhaps you got two queries that run individually and want to invalidate the cache only for one of them. + +To do this, overwrite your `getCacheTagsToInvalidateOnUpdate()` method in your model: + +```php +class Page extends Model +{ + use QueryCacheable; + + /** + * Invalidate the cache automatically + * upon update in the database. + */ + protected static $flushCacheOnUpdate = true; + + /** + * When invalidating automatically on update, you can specify + * which tags to invalidate. + * + * @return array + */ + public function getCacheTagsToInvalidateOnUpdate(): array + { + return [ + 'query1', + ]; + } +} + +$query1 = Page::cacheFor(60) + ->cacheTags(['query1']) + ->get(); + +$query2 = Page::cacheFor(60) + ->cacheTags(['query2']) + ->get(); + +// The $query1 gets invalidated +// but $query2 will still hit from cache if re-called. + +$page = Page::first(); + +$page->update([ + 'name' => 'Reddit', +]); +``` + +**Please keep in mind: Setting `$flushCacheOnUpdate` to `true` and not specifying individual tags to invalidate will lead to [Full Automatic Invalidation](#full-automatic-invalidation) since the default tags to invalidate are the base tags and you need at least one tag to invalidate.** + +**Not specifying a tag to invalidate fallbacks to the set of base tags, thus leading to Full Automatic Invalidation.** + ## Cache Keys The package automatically generate the keys needed to store the data in the cache store. However, prefixing them might be useful if the cache store is used by other applications and/or models and you want to manage the keys better to avoid collisions. diff --git a/database/factories/PageFactory.php b/database/factories/PageFactory.php new file mode 100644 index 0000000..bb93187 --- /dev/null +++ b/database/factories/PageFactory.php @@ -0,0 +1,19 @@ +define(\Rennokki\QueryCache\Test\Models\Page::class, function () { + return [ + 'name' => 'Page'.Str::random(5), + ]; +}); diff --git a/src/FlushQueryCacheObserver.php b/src/FlushQueryCacheObserver.php new file mode 100644 index 0000000..9b4aec5 --- /dev/null +++ b/src/FlushQueryCacheObserver.php @@ -0,0 +1,83 @@ +invalidateCache($model); + } + + /** + * Handle the Model "updated" event. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return void + */ + public function updated(Model $model) + { + $this->invalidateCache($model); + } + + /** + * Handle the Model "deleted" event. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return void + */ + public function deleted(Model $model) + { + $this->invalidateCache($model); + } + + /** + * Handle the Model "forceDeleted" event. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return void + */ + public function forceDeleted(Model $model) + { + $this->invalidateCache($model); + } + + /** + * Handle the Model "restored" event. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return void + */ + public function restored(Model $model) + { + $this->invalidateCache($model); + } + + /** + * Invalidate the cache for a model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return void + * @throws Exception + */ + protected function invalidateCache(Model $model): void + { + if (! $model->getCacheTagsToInvalidateOnUpdate()) { + throw new Exception('Automatic invalidation for '.$class.' works only if at least one tag to be invalidated is specified.'); + } + + $class = get_class($model); + + $class::flushQueryCache( + $model->getCacheTagsToInvalidateOnUpdate() + ); + } +} diff --git a/src/Traits/QueryCacheable.php b/src/Traits/QueryCacheable.php index 1b6dff5..bec66da 100644 --- a/src/Traits/QueryCacheable.php +++ b/src/Traits/QueryCacheable.php @@ -2,10 +2,50 @@ namespace Rennokki\QueryCache\Traits; +use Illuminate\Database\Eloquent\Model; +use Rennokki\QueryCache\FlushQueryCacheObserver; use Rennokki\QueryCache\Query\Builder; trait QueryCacheable { + /** + * Get the observer class name that will + * observe the changes and will invalidate the cache + * upon database change. + * + * @return string + */ + protected static function getFlushQueryCacheObserver() + { + return FlushQueryCacheObserver::class; + } + + /** + * When invalidating automatically on update, you can specify + * which tags to invalidate. + * + * @return array + */ + public function getCacheTagsToInvalidateOnUpdate(): array + { + return $this->getCacheBaseTags(); + } + + /** + * {@inheritdoc} + * + */ + public static function boot() + { + parent::boot(); + + if (isset(static::$flushCacheOnUpdate) && static::$flushCacheOnUpdate) { + static::observe( + static::getFlushQueryCacheObserver() + ); + } + } + /** * {@inheritdoc} */ diff --git a/tests/FlushCacheOnUpdateTest.php b/tests/FlushCacheOnUpdateTest.php new file mode 100644 index 0000000..bedd376 --- /dev/null +++ b/tests/FlushCacheOnUpdateTest.php @@ -0,0 +1,94 @@ +create(); + $storedPage = Page::cacheFor(now()->addHours(1))->first(); + $cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}'); + + $this->assertNotNull($cache); + + $this->assertEquals( + $cache->first()->id, + $storedPage->id + ); + + Page::create([ + 'name' => '9GAG', + ]); + + $cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}'); + + $this->assertNull($cache); + } + + public function test_flush_cache_on_update() + { + $page = factory(Page::class)->create(); + $storedPage = Page::cacheFor(now()->addHours(1))->first(); + $cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}'); + + $this->assertNotNull($cache); + + $this->assertEquals( + $cache->first()->id, + $storedPage->id + ); + + $page->update([ + 'name' => '9GAG', + ]); + + $cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}'); + + $this->assertNull($cache); + } + + public function test_flush_cache_on_delete() + { + $page = factory(Page::class)->create(); + $storedPage = Page::cacheFor(now()->addHours(1))->first(); + $cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}'); + + $this->assertNotNull($cache); + + $this->assertEquals( + $cache->first()->id, + $storedPage->id + ); + + $page->delete(); + + $cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}'); + + $this->assertNull($cache); + } + + public function test_flush_cache_on_force_deletion() + { + $page = factory(Page::class)->create(); + $storedPage = Page::cacheFor(now()->addHours(1))->first(); + $cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}'); + + $this->assertNotNull($cache); + + $this->assertEquals( + $cache->first()->id, + $storedPage->id + ); + + $page->forceDelete(); + + $cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}'); + + $this->assertNull($cache); + } +} diff --git a/tests/Models/Page.php b/tests/Models/Page.php new file mode 100644 index 0000000..d9de9a8 --- /dev/null +++ b/tests/Models/Page.php @@ -0,0 +1,26 @@ +set('auth.providers.posts.model', Post::class); $app['config']->set('auth.providers.kids.model', Kid::class); $app['config']->set('auth.providers.books.model', Book::class); + $app['config']->set('auth.providers.pages.model', Page::class); $app['config']->set('app.key', 'wslxrEFGWY6GfGhvN9L3wH3KSRJQQpBD'); } diff --git a/tests/database/migrations/2018_07_14_183253_pages.php b/tests/database/migrations/2018_07_14_183253_pages.php new file mode 100644 index 0000000..84740de --- /dev/null +++ b/tests/database/migrations/2018_07_14_183253_pages.php @@ -0,0 +1,32 @@ +increments('id'); + $table->string('name'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('pages'); + } +} From 85b06b2625ee89cc7c47f5eb5c8d38fb64018175 Mon Sep 17 00:00:00 2001 From: rennokki Date: Mon, 27 Apr 2020 15:05:04 +0000 Subject: [PATCH 2/3] Apply fixes from StyleCI --- src/Traits/QueryCacheable.php | 2 -- tests/FlushCacheOnUpdateTest.php | 1 - 2 files changed, 3 deletions(-) diff --git a/src/Traits/QueryCacheable.php b/src/Traits/QueryCacheable.php index bec66da..94f3982 100644 --- a/src/Traits/QueryCacheable.php +++ b/src/Traits/QueryCacheable.php @@ -2,7 +2,6 @@ namespace Rennokki\QueryCache\Traits; -use Illuminate\Database\Eloquent\Model; use Rennokki\QueryCache\FlushQueryCacheObserver; use Rennokki\QueryCache\Query\Builder; @@ -33,7 +32,6 @@ public function getCacheTagsToInvalidateOnUpdate(): array /** * {@inheritdoc} - * */ public static function boot() { diff --git a/tests/FlushCacheOnUpdateTest.php b/tests/FlushCacheOnUpdateTest.php index bedd376..7d5988b 100644 --- a/tests/FlushCacheOnUpdateTest.php +++ b/tests/FlushCacheOnUpdateTest.php @@ -4,7 +4,6 @@ use Cache; use Rennokki\QueryCache\Test\Models\Page; -use Rennokki\QueryCache\Test\Models\Post; class FlushCacheOnUpdateTest extends TestCase { From 9e28beaf0910a84627996f1ac74f4ba1b89b254c Mon Sep 17 00:00:00 2001 From: Alex Renoki Date: Mon, 27 Apr 2020 18:08:54 +0300 Subject: [PATCH 3/3] Updated readme --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 598fcf4..c644106 100644 --- a/README.md +++ b/README.md @@ -135,24 +135,9 @@ class Kid extends Model Kid::flushQueryCache(); ``` -## Relationship Caching - -Relationships are just another queries. They can be intercepted and modified before the database is hit with the query. The following example needs the `Order` model (or the model associated with the `orders` relationship) to include the `QueryCacheable` trait. - -```php -$user = User::with(['orders' => function ($query) { - return $query - ->cacheFor(60 * 60) - ->cacheTags(['my:orders']); -}])->get(); - -// This comes from the cache if existed. -$orders = $user->orders; -``` - ## Full Automatic Invalidation -To speed up the scaffolding of invalidation within your app, you can specify the model to auto-flush the cache upon any model gets created, updated or deleted. +To speed up the scaffolding of invalidation within your app, you can specify the model to auto-flush the cache upon any time records gets created, updated or deleted. ```php class Page extends Model @@ -224,6 +209,21 @@ $page->update([ **Not specifying a tag to invalidate fallbacks to the set of base tags, thus leading to Full Automatic Invalidation.** +## Relationship Caching + +Relationships are just another queries. They can be intercepted and modified before the database is hit with the query. The following example needs the `Order` model (or the model associated with the `orders` relationship) to include the `QueryCacheable` trait. + +```php +$user = User::with(['orders' => function ($query) { + return $query + ->cacheFor(60 * 60) + ->cacheTags(['my:orders']); +}])->get(); + +// This comes from the cache if existed. +$orders = $user->orders; +``` + ## Cache Keys The package automatically generate the keys needed to store the data in the cache store. However, prefixing them might be useful if the cache store is used by other applications and/or models and you want to manage the keys better to avoid collisions.