-
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ba99c92
commit 5ece884
Showing
8 changed files
with
325 additions
and
312 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,7 +39,7 @@ $database = DB::table('articles')->latest('published_at')->take(10)->cache()->ge | |
$eloquent = Article::latest('published_at')->take(10)->cache()->get(); | ||
``` | ||
|
||
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`. | ||
The next time you call the **same** query, the result will be retrieved from the cache instead of running the `SELECT` SQL statement in the database, even if the results are empty, `null` or `false`. | ||
|
||
Since it's [eager load unaware](#eager-load-unaware), you can also cache (or not) an eager loaded relation. | ||
|
||
|
@@ -96,34 +96,20 @@ Article::latest('published_at')->take(200)->cache(wait: 5)->get(); | |
|
||
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. | ||
|
||
> If you need to use this across multiple processes, use the [cache lock](https://laravel.com/docs/cache#managing-locks-across-processes) directly. | ||
### Idempotent queries | ||
|
||
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. | ||
|
||
For example, you can make this query only execute once every day for a given user ID. | ||
|
||
```php | ||
$key = auth()->user()->getAuthIdentifier(); | ||
|
||
Article::whereKey(54)->cache(now()->addHour(), "user:$key")->increment('unique_views'); | ||
``` | ||
|
||
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. | ||
> If you need a more advanced locking mechanism, use the [cache lock](https://laravel.com/docs/cache#managing-locks-across-processes) directly. | ||
## Forgetting results | ||
|
||
If you need to forget a result anywhere in your application, you should use a named key for your cached query, as autogenerated query keys are difficult (or plain impossible) to guess. Once you do, use the `cache-query:forget` command with the name of the key. | ||
|
||
```php | ||
User::query()->cache(key: 'find_joe')->whereName('Joe')->whereAge(20)->first(); | ||
User::query()->cache(store: 'redis', key: 'find_joe')->whereName('Joe')->whereAge(20)->first(); | ||
``` | ||
|
||
```bash | ||
php artisan cache-query:forget find_joe | ||
php artisan cache-query:forget find_joe --store=redis | ||
|
||
# Successfully removed [find_joe] from the [file] cache store. | ||
# Successfully removed [find_joe] from the [redis] cache store. | ||
``` | ||
|
||
## Caveats | ||
|
@@ -153,7 +139,7 @@ User::query()->cache(key: 'find_joe')->whereAge(20)->whereName('Joe')->first(); | |
|
||
### Eager load **unaware** | ||
|
||
Since caching only works for the current query builder instance, an underlying Eager Load query won't be cached. This may be a blessing or a curse depending on your scenario. | ||
Since caching only works for the current query builder instance, an underlying Eager Load query won't be cached, as it's executed later in the pipeline. This may be a blessing or a curse depending on your scenario. | ||
|
||
```php | ||
$page = 1; | ||
|
@@ -163,7 +149,7 @@ User::with('posts', function ($posts) use ($page) { | |
})->cache()->find(1); | ||
``` | ||
|
||
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. | ||
In the example, the `posts` eager load query results are never cached. To avoid that, you may 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. | ||
|
||
```php | ||
$page = 1; | ||
|
@@ -173,9 +159,21 @@ User::with('posts', function ($posts) use ($page) { | |
})->find(1); | ||
``` | ||
|
||
Alternatively, you can cache the whole results manually using `remember()`: | ||
|
||
```php | ||
$page = 1; | ||
|
||
cache()->remember('cache_whole_results', function () use ($page) { | ||
User::with('posts', function ($posts) use ($page) { | ||
return $posts()->cache()->forPage($page); | ||
})->find(1); | ||
}) | ||
``` | ||
|
||
### Cannot delete autogenerated keys | ||
|
||
When the cache auto-generates a query key, [it's hashed](#operations-are-not-commutative), making it extremely difficult to remove it from the cache. If you need to remove the results from the cache at any given time, use a custom key and [use the included `cache-query:forget` command](#forgetting-results): | ||
When a key is not issued, the cache crates a hash based on the query, making it extremely difficult to remove it from the cache. If you need to remove the results from the cache at any given time, you should use a custom key and [the included `cache-query:forget` command](#forgetting-results). | ||
|
||
## PhpStorm stubs | ||
|
||
|
@@ -189,11 +187,9 @@ The file gets published into the `.stubs` folder of your project. You should poi | |
|
||
## How it works? | ||
|
||
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. | ||
When you use `cache()`, it will wrap the connection into a `CacheAwareProxy` proxy, and proxy all method calls to it. | ||
|
||
This callback will check 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. | ||
|
||
For the Eloquent Builder, this wraps happens below it, so all calls pass through the `CacheAwareProxy` before hitting the real base builder. | ||
Once a `SELECT` statement is executed, it will check if the results are in the cache before executing the query. On Cache hit, it will return the cached results. | ||
|
||
## Laravel Octane compatibility | ||
|
||
|
@@ -204,6 +200,8 @@ For the Eloquent Builder, this wraps happens below it, so all calls pass through | |
|
||
There should be no problems using this package with Laravel Octane. | ||
|
||
## [Upgrading](UPGRADE.md) | ||
|
||
## Security | ||
|
||
If you discover any security related issues, please email [email protected] instead of using the issue tracker. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Upgrading | ||
|
||
## From 1.x | ||
|
||
### Idempotent queries | ||
|
||
[Idempotent](https://en.wikipedia.org/wiki/Idempotence) queries have been removed. Cached queries only work for `SELECT` procedures, like `first()` or `get()`. | ||
|
||
As an alternative, you can use `remember()` from your application cache for the same effect: | ||
|
||
```php | ||
cache()->remember('idempotent', function () { | ||
Article::whereKey(10)->increment('unique_views'); | ||
|
||
return true; | ||
}) | ||
``` | ||
|
||
### Query Hash | ||
|
||
If for some reason you where using the query hashes, 2.x incorporates the connection name into the hash. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
<?php | ||
|
||
namespace Laragear\CacheQuery; | ||
|
||
use DateInterval; | ||
use DateTimeInterface; | ||
use Illuminate\Cache\NoLock; | ||
use Illuminate\Contracts\Cache\Lock; | ||
use Illuminate\Contracts\Cache\LockProvider; | ||
use Illuminate\Contracts\Cache\Repository; | ||
use Illuminate\Database\ConnectionInterface; | ||
use LogicException; | ||
use function array_shift; | ||
use function base64_encode; | ||
use function cache; | ||
use function config; | ||
use function implode; | ||
use function md5; | ||
|
||
class CacheAwareConnectionProxy | ||
{ | ||
/** | ||
* Create a new Cache Aware Connection Proxy instance. | ||
* | ||
* @param \Illuminate\Database\ConnectionInterface $connection | ||
* @param \Illuminate\Contracts\Cache\Repository $repository | ||
* @param string $cachePrefix | ||
* @param string $queryKey | ||
* @param \DateTimeInterface|\DateInterval|int $ttl | ||
* @param int $lockWait | ||
*/ | ||
public function __construct( | ||
public ConnectionInterface $connection, | ||
protected Repository $repository, | ||
protected string $cachePrefix, | ||
protected string $queryKey, | ||
protected DateTimeInterface|DateInterval|int $ttl, | ||
protected int $lockWait, | ||
) { | ||
// | ||
} | ||
|
||
/** | ||
* Run a select statement against the database. | ||
* | ||
* @param string $query | ||
* @param array $bindings | ||
* @param bool $useReadPdo | ||
* @return array | ||
*/ | ||
public function select($query, $bindings = [], $useReadPdo = true): mixed | ||
{ | ||
$key = $this->cachePrefix.'|'.($this->queryKey ?: $this->getQueryHash($query, $bindings)); | ||
|
||
[$cached, $results] = $this->checkForCachedResult($key); | ||
|
||
if ($cached) { | ||
return $results; | ||
} | ||
|
||
$results = $this->connection->select($query, $bindings, $useReadPdo); | ||
|
||
$this->repository->put($key, $results, $this->ttl); | ||
|
||
return $results; | ||
} | ||
|
||
/** | ||
* Run a select statement and return a single result. | ||
* | ||
* @param string $query | ||
* @param array $bindings | ||
* @param bool $useReadPdo | ||
* @return mixed | ||
*/ | ||
public function selectOne($query, $bindings = [], $useReadPdo = true) | ||
{ | ||
$records = $this->select($query, $bindings, $useReadPdo); | ||
|
||
return array_shift($records); | ||
} | ||
|
||
/** | ||
* Checks if the result exists in the cache, and returns in. | ||
* | ||
* @param string $key | ||
* @return mixed | ||
*/ | ||
protected function checkForCachedResult(string $key): mixed | ||
{ | ||
return $this->retrieveLock($key)->block($this->lockWait, function () use ($key) { | ||
$has = $this->repository->has($key); | ||
|
||
return [$has, $has ? $this->repository->get($key) : null]; | ||
}); | ||
} | ||
|
||
/** | ||
* Hashes the incoming query for using as cache key. | ||
* | ||
* @param string $query | ||
* @param array $bindings | ||
* @return string | ||
*/ | ||
protected function getQueryHash(string $query, array $bindings): string | ||
{ | ||
return base64_encode(md5($this->connection->getDatabaseName().$query.implode('', $bindings), true)); | ||
} | ||
|
||
/** | ||
* Retrieves the lock to use before getting the results. | ||
* | ||
* @param string $key | ||
* @return \Illuminate\Contracts\Cache\Lock | ||
*/ | ||
protected function retrieveLock(string $key): Lock | ||
{ | ||
if (!$this->lockWait) { | ||
return new NoLock($key, $this->lockWait); | ||
} | ||
|
||
return $this->repository->getStore()->lock($key, $this->lockWait); | ||
} | ||
|
||
/** | ||
* Pass-through all properties to the underlying connection. | ||
* | ||
* @param string $name | ||
* @return mixed | ||
*/ | ||
public function __get(string $name): mixed | ||
{ | ||
return $this->connection->{$name}; | ||
} | ||
|
||
/** | ||
* Pass-through all properties to the underlying connection. | ||
* | ||
* @param string $name | ||
* @param mixed $value | ||
* @return void | ||
* @noinspection MagicMethodsValidityInspection | ||
*/ | ||
public function __set(string $name, mixed $value): void | ||
{ | ||
$this->connection->{$name} = $value; | ||
} | ||
|
||
/** | ||
* Pass-through all method calls to the underlying connection. | ||
* | ||
* @param string $name | ||
* @param array $arguments | ||
* @return mixed | ||
*/ | ||
public function __call(string $name, array $arguments) | ||
{ | ||
return $this->connection->{$name}(...$arguments); | ||
} | ||
|
||
/** | ||
* Create a new CacheAwareProxy instance. | ||
* | ||
* @param \Illuminate\Database\ConnectionInterface $connection | ||
* @param \DateTimeInterface|\DateInterval|int $ttl | ||
* @param string $key | ||
* @param int $lockWait | ||
* @param string|null $store | ||
* @return static | ||
*/ | ||
public static function crateNewInstance( | ||
ConnectionInterface $connection, | ||
DateTimeInterface|DateInterval|int $ttl, | ||
string $key, | ||
int $lockWait, | ||
?string $store, | ||
): static { | ||
$repository = static::store($store, (bool) $lockWait); | ||
|
||
return new static($connection, $repository, config('cache-query.prefix'), $key, $ttl, $lockWait); | ||
} | ||
|
||
/** | ||
* Returns the store to se for caching. | ||
* | ||
* @param string|null $store | ||
* @param bool $lockable | ||
* @return \Illuminate\Contracts\Cache\Repository | ||
*/ | ||
protected static function store(?string $store, bool $lockable = false): Repository | ||
{ | ||
$repository = cache()->store($store ?? config('cache-query.store')); | ||
|
||
if ($lockable && !$repository->getStore() instanceof LockProvider) { | ||
$store ??= cache()->getDefaultDriver(); | ||
|
||
throw new LogicException("The [$store] cache does not support atomic locks."); | ||
} | ||
|
||
return $repository; | ||
} | ||
} |
Oops, something went wrong.