Laravel: I cached my queries with only one method

I shouldn’t have to, but I did it anyway

Italo Baeza Cabrera
4 min readFeb 18, 2022
Photo by CHUTTERSNAP on Unsplash

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

Gotta cache ’em all.

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 select().

The one method that will actually retrieve the results from the database.

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 tap() method.

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 proxy or wrapper, simplified.

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.

Nobody will notice which query is.

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 array.

A very simplified implementation, but it will work nonetheless.

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.

Some people may want to do this instead of caching the whole endeavor.

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.

You can also go the extra mile and support me on Patreon, Ko-Fi, Buy me a coffee or just PayPal.

--

--

Italo Baeza Cabrera
Italo Baeza Cabrera

Written by Italo Baeza Cabrera

Graphic Designer graduate. Full Stack Web Developer. Retired Tech & Gaming Editor. https://italobc.com

Responses (2)