5.1 efficient eloquent cache?

Published 3 years ago by Seteve09

Hi, Ive been using repositories pattern for my eloquent/db queries. I have to implement Cache::remember at anything that requires caching, plus I need to manually flush the cache if the model is updated.

Example, Each user has a profile.

Normally I will eager load this

$users = $this->userRepo->getUsers();

//in repo
User::with('profile')->get();

Well this will run two queries every time. One for the user, another is the eager loaded profile.

Well this is good. But I still need to cache them. Like i stated above, i am doing Cache::remember on all the eloquent queries, and then if a user updated his/her profile, i need to flush the cache.

This is not maintanable at all if my application gets big.

Ive been looking at some observer pattern: http://laravel-tricks.com/tricks/easy-eloquent-relation-cache-management

I also dug into the Model class in Laravel 5.1, noticed upon a model deleted/saved, there will be an event fired.

So anyone here can point me at the correct direction?

pmall
pmall
3 years ago (547,345 XP)

What the problem exactly ? You dont know how to flush cache when user update his profile ?

If so then you can name the cache based on the updated_at column of the profile.

JarekTkaczyk

@Seteve09 Create a new repo that handles cache. It will be a decorator for the repository that you already have and will hold all the logic responsible for deciding whether the cache can be used or needs to be updated (by calling the main, eloquent repo).

Seteve09

@JarekTkaczyk thanks, is there any examples that i can refer to?

JarekTkaczyk

@Seteve09 You could try https://laracasts.com/lessons/decorating-repositories - though I don't know this video and can't guarantee. Also @fideloper wrote about it in his book about laravel if I remember right, so google it.

rbruhn

@Seteve09 Came across your question when implementing my own cache solution and wanted to share my methods. It was put together using ideas from Laracast, @fideloper , and others on the net over the past year or two. It uses decorators and can handle relationships. I removed any phpdoc for easier reading.

MyServiceProvider - The 'model' string in the decorator is a cache tag. Can be anything you want.

use App\Models\Group;
use App\Repositories\GroupRepository;
use App\Repositories\Contracts\Group as GroupContract;
use App\Repositories\Decorators\CacheGroupDecorator;
....

protected function registerRepositories()
{
    $this->app->singleton(GroupContract::class, function () {
        $group = new GroupRepository(new Group);
        $cache = new CacheGroupDecorator($this->app['cache.store'], $group, 'model');

         return $cache;
    }); 
}

Group Model - Nothing different here than your typical model.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Group extends Model
{
    use SoftDeletes;

    protected $dates = ['deleted_at'];
    protected $table = 'groups';
    protected $softDelete = true;
    protected $fillable = [
        'user_id',
        'name',
        'label',
    ];

    public function projects()
    {
        return $this->hasMany(Project::class)->orderBy('title');
    }

    public function owner()
    {
        return $this->belongsTo(User::class, 'user_id');
    }

// Contains typical model stuff ....
}

I use an abstract Repo class to handle common calls (find(), all(), create()). In regard to your question, you should look at the findWith and make methods.

namespace App\Repositories;

abstract class Repository
{
    protected $model;

    public function all()
    {
        return $this->model->all();
    }

    public function find($id)
    {
        return $this->model->find($id);
    }

    public function findWith($id, $with)
    {
        $query = $this->make($with);

        return $query->find($id);
    }

    public function make($with = [])
    {
        return $this->model->with($with);
    }

......
}

Group - Interface extending a Repository interface that holds required methods.

namespace App\Repositories\Contracts;

interface Group extends Repository
{
    public function findByName($name);
}

GroupRepository - Extends the Repository.php class. Implements Group interface.

namespace App\Repositories;

use App\Repositories\Contracts\Group;
use App\Models\Group as Model;

class GroupRepository extends Repository implements Group
{
    public function __construct(Model $model)
    {
        $this->model = $model;
    }

    // Other functions that are needed expressed in Group interface
}

CacheDecorator - Simply a class I extend for sharing common code.

namespace App\Repositories\Decorators;

use Illuminate\Contracts\Cache\Repository as Cache;

class CacheDecorator
{
    protected $repository;
    protected $cache;
    protected $tag;
    protected $cached = true;

    public function __construct(Cache $cache, $repository, $tag)
    {
        $this->repository = $repository;
        $this->cache = $cache;
        $this->tag = $tag;
    }

    public function cached($bool = true)
    {
        $this->cached = $bool;
    }
}

CacheGroupDecorator - Where the work gets done. Only posting a couple method examples.

namespace App\Repositories\Decorators;

use App\Repositories\Contracts\Group;

class CacheGroupDecorator extends CacheDecorator implements Group
{

    private function buildKey($method, $identifier = null)
    {
        return md5($method . $identifier);
    }

    public function all()
    {
        if ( ! $this->cached) {
            return $this->repository->all();
        }

        // __METHOD__ returns App\Repositories\Decorators\CacheGroupDecorator::all so not confused with other all() methods.
        $key = $this->buildKey(__METHOD__);

        return $this->cache->tags($this->tag)->rememberForever($key, function () {
            return $this->repository->all();
        });
    }

    public function find($id)
    {
        if ( ! $this->cached) {
            return $this->repository->find($id);
        }

        $key = $this->buildKey(__METHOD__, $id);

        return $this->cache->tags($this->tag)->rememberForever($key, function () use ($id) {
            return $this->repository->find($id);
        });
    }

    public function findWith($id, $with)
    {
        if ( ! $this->cached) {
            return $this->repository->findWith($id, $with);
        }

        $key = $this->buildKey(__METHOD__, $id . implode('.', $with));

        return $this->cache->tags($this->tag)->rememberForever($key, function () use ($id, $with) {
            return $this->repository->findWith($id, $with);
        });
    }
// Other methods ......
}

As you can see, I use rememberForever with the cache tag. When saving/updating/deleting I flush the cache.

In a controller, I can do something like this:

public function index()
{
        // To retrieve fresh data, not using cache, set cached to false.
        $this->group->cached(false);
        $group = $this->group->all();

       // Retrieve cached data if it exists using relationships. If not cached, it will use the repo and cache results.
       $group = $this->group->findWith($id, ['owner', 'projects']);

        return view('group.show', $group);
}

So far, this has been the best method for me.

Edit: After posting this, and not having looked at it in a long while, I realized I'm duplicating a lot of code in every repo decorator I create. So, I moved all my common methods (all, find, findWith, save, create, update, destroy, buildKey) to the CacheDecorator class and use a $key property. So with all those common methods in the CacheDecorator, I merely set the cache key and call the parent. Any custom repo specific methods can simply be added to the specific decorator. CacheGroupDecorator

    public function all()
    {
        $this->buildKey(__METHOD__);

        return parent::all();
    }

    public function find($id)
    {
        $this->buildKey(__METHOD__, $id);

        return parent::find($id);
    }

    public function findWith($id, $with)
    {
        $this->buildKey(__METHOD__, $id . implode('.', $with));

        return parent::findWith($id, $with);
    }

Anyway, maybe this will help others.

kureci

@rbruhn I'm looking for a similar solution and yours seems to be quite comprehensive. However, as I'm still learning Laravel bits, I'm not sure where each of those classes go. Can you please elaborate a bit more on locations of the files? Also, in your 'MyServiceProvider', you've got the ... there:

use App\Models\Group;
use App\Repositories\GroupRepository;
use App\Repositories\Contracts\Group as GroupContract;
use App\Repositories\Decorators\CacheGroupDecorator;
....

which I'm not sure what it should contain. Do you perhaps have a complete tutorial somewhere that you can give the link here? Or can you just fit in the missing parts in here?

Thanks!

rbruhn

@kureci - Hi... You can see where I place each of the classes by reading the namespace. In the MyServiceProvider, I used the ... because there are simply a lot of references to the classes used when binding the repos. So, for example, my providers extends ServiceProvider and looks like this (not including all the classes listed above the .... ):

class MyServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->registerViewComposer();
        $this->registerRepositories();
    }

    protected function registerViewComposer()
    {
        $this->app->make('view')->composer('front.layouts.default', TopMenuComposer::class);
    }

    protected function registerRepositories()
    {
        $this->app->singleton(UserContract::class, function () {
            $user = new UserRepository(new User);
            $cache = new CacheUserDecorator($this->app['cache.store'], $user, 'model');

            return $cache;
        });

        $this->app->singleton(GroupContract::class, function () {
            $group = new GroupRepository(new Group);
            $cache = new CacheGroupDecorator($this->app['cache.store'], $group, 'model');

            return $cache;
        });

        $this->app->singleton(PermissionContract::class, function () {
            $permission = new PermissionRepository(new Permission);
            $cache = new CacheGroupDecorator($this->app['cache.store'], $permission, 'model');

            return $cache;
        });

        $this->app->bind(ProjectContract::class, function () {
            $project = new ProjectRepository(new Project);
            $cache = new CacheProjectDecorator($this->app['cache.store'], $project, 'model');

            return $cache;

        });

        $this->app->bind(ExpeditionContract::class, function () {
            $expedition = new ExpeditionRepository(new Expedition);
            $cache = new CacheExpeditionDecorator($this->app['cache.store'], $expedition, 'model');

            return $cache;
        });

        $this->app->bind(MetaContract::class, MetaRepository::class);
        $this->app->bind(OcrQueueContract::class, OcrQueueRepository::class);
        $this->app->bind(TranscriptionContract::class, TranscriptionRepository::class);
    }
}

Please sign in or create an account to participate in this conversation.