Laravel: I cached my queries with only one method
I shouldn’t have to, but I did it anyway
Long time ago, around 2013, Laravel included the
remember() method in the Query Builder. The functionality was simple: remember the query results for a given amount of time. That method was killed in 2015, when Laravel 5.0 was introduced.
I was one of the developers who understood the decision behind it, and considering how niche was to cache a query back in the day, almost nobody missed it, but still it was a major convenience that is now gone.
I wanted that functionality back, and for that I decided to migrate a package to Laragear that cached queries with one method, but not using the hacky way to do it, but a new one.
Proxying the Database Connection
What you see here is the new CacheQuery 2.x in action. It registers a single Builder macro called
cache(), and that’s it. Behind it, magic happens.
Okay, not magic, but sort of. The base Query Builder contains an instance of the Database Connection, with a lot of methods to execute directly in the real database. The method we’re interested in is called
One clean way to capture a
select() call is basically using a proxy object.
A “proxy” is basically a class that wraps another object, and forwards all method and properties to it. You can override everything, you can replace one method, etc. One commonly used proxy is the one returned by the
This proxy object has one task: wrap the Connection instance and, only when
select() is called, check if there is an already cached results from the same query. We can just override the
select() method with our own logic, while forwarding everything else.
The above allows for a very clean way to cache results without having to add traits, alter the Connection instance or having to inject callbacks in the Query Builder itself, which are not reliable ways to intercept calls without annoying the Database Connection. The prior version had to use an Exception to do what we now do in a few lines.
Of course there is more to it, but the principle is the same: before the Query Builder sends the query to
select(), we will check if the cache has already the query results. For that, I decided to allow the developer to use its own key, like
find_joe, but also do it automatically by hashing the SQL statement itself.
By hashing the query with MD5 and BASE64 we can get smaller cache keys, but also reproducible ones. This same query will yield the same key as long it doesn’t change.
Finally, when calling
select(), we can safely return the cached results if these exist, or just proceed with the real query execution and save the incoming
There are some caveats here and there, but minor ones considering how clean the implementation is, and how it doesn’t meddles with the Query Builder and Connection too much to introduce important edge cases.
For example, one I already sorted out in local development is Eager Loading awareness, and the only thing stopping me is minor cosmetic feature. In the meantime, you can selectively cache an eager loaded query.
There is still room for improvement, specially on the Eloquent side of things, but I call this mission accomplished. Next time I need to cache something, I will just seven keystrokes away.
If you also want to cache your queries in one method, just install CacheQuery in your project by visiting the repository below with the instructions.