Globally Eager Loading on Auth::user() to prevent n+1 problem

Published 1 month ago by rema96

I am building a subscription based site, I am using cashier and I got everything setup also I have a plans table that represents what I have on Stripe, this is simply because I want to list price plans. Furthermore I have features, features and plans are connected with a pivot table n-m.

The problem I am having I would like to know if user has a certain feature, I get this bases on his subscription and then his plan connected to that subscription.

In users model I made a simple function to test this:

public function features()
    {
        return User::join('subscriptions', 'users.id', '=', 'subscriptions.user_id')
            ->join('plans', 'plans.id', '=', 'subscriptions.stripe_plan')
            ->join('feature_plan', 'feature_plan.plan_id', '=', 'plans.id')
            ->join('features', 'features.id', '=', 'feature_plan.feature_id')
            ->where('users.id', '=', $this->id)
            ->get();
    }

With this I get all the features of that user, I also have my relationships setup in Laravel so this App\User::find(1)->subscription()->plan->features->toArray() also works.

Here is a problem if I call hasFeature() function in a loop or even multiple times on the page I will get the famous n+1 problem.

public function hasFeature($feature_id)
    {
        return $this->features() // This will call the function above
                    ->where('slug', '=', $feature_id)
                    ->isNotEmpty();
    }

I know the way to solve this is with eager loading but how can I globally eager load this information to my user?

One idea I had was retrieve all the features in User model constructor and assign it to a variable in the class, but I am not really sure if that will work.

Also is there a way to optimize this even more, like cache it so it doesn't make a request every single page load?

Best Answer (As Selected By rema96)
rema96

I gave it a bit of thought and solved it.

As Cronix said, subscription() isn't returning a relationship. Therefore I made my own function sub() which returns a relationship, then I user $with attribute. protected $with = ['sub.plan.features'];

public function sub()
    {
        return $this->hasOne('App\Subscription');
    }

Thank you both for helping!

bobbybouwmann

You have a property on a model where you can automatically load relationships. It's called with and looks like this in your case

class User extends Authenticatable
{
    /**
     * The relations to eager load on every query.
     *
     * @var array
     */
    protected $with = ['subscription.plan.features'];
}

Now the user should always have these properties available if they have any!

Another solution is using caching inside your hasFeature method or any other method where you fetch it.

public function hasFeature($feature_id)
{
    Cache::remember('feature-' . $feature_id . '-' . auth()->id(), $minutes, function () use ($feature_id) {
        return $this->features()
            ->where('slug', '=', $feature_id)
            ->isNotEmpty();
    });
}

The first option looks nice, but then if you update something in one of the models your user has you might need to reload the relations of the user otherwise you get weird results.

Let me know if any of this works for you!

Cronix
Cronix
1 month ago (571,210 XP)

You can use the $with array on the user table. It will autoload the relationships whenever a user is retrieved.

public $with = ['relationshipName'];

It will only work with relationships though, like if you can do User::with('relationshipName'), then you can also do the above, but it's automatic.

rema96

I forgot to state that I tried $with but with no luck...

I get:

BadMethodCallException thrown with message "Method Illuminate\Database\Query\Builder::with does not exist."

Stacktrace:
#57 BadMethodCallException in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php:2813
#56 Illuminate\Database\Query\Builder:__call in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:558
#55 Illuminate\Database\Eloquent\Builder:getRelation in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:518
#54 Illuminate\Database\Eloquent\Builder:eagerLoadRelation in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:498
#53 Illuminate\Database\Eloquent\Builder:eagerLoadRelations in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:466
#52 Illuminate\Database\Eloquent\Builder:get in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Database/Concerns/BuildsQueries.php:77
#51 Illuminate\Database\Eloquent\Builder:first in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Auth/EloquentUserProvider.php:51
#50 Illuminate\Auth\EloquentUserProvider:retrieveById in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php:132
#49 Illuminate\Auth\SessionGuard:user in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Auth/GuardHelpers.php:36
#48 Illuminate\Auth\SessionGuard:authenticate in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Auth/AuthManager.php:292
#47 Illuminate\Auth\AuthManager:__call in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php:57
#46 Illuminate\Auth\Middleware\Authenticate:authenticate in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php:41
#45 Illuminate\Auth\Middleware\Authenticate:handle in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
#44 Illuminate\Pipeline\Pipeline:Illuminate\Pipeline\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
#43 Illuminate\Routing\Pipeline:Illuminate\Routing\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php:67
#42 Illuminate\Foundation\Http\Middleware\VerifyCsrfToken:handle in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
#41 Illuminate\Pipeline\Pipeline:Illuminate\Pipeline\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
#40 Illuminate\Routing\Pipeline:Illuminate\Routing\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/View/Middleware/ShareErrorsFromSession.php:49
#39 Illuminate\View\Middleware\ShareErrorsFromSession:handle in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
#38 Illuminate\Pipeline\Pipeline:Illuminate\Pipeline\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
#37 Illuminate\Routing\Pipeline:Illuminate\Routing\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php:63
#36 Illuminate\Session\Middleware\StartSession:handle in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
#35 Illuminate\Pipeline\Pipeline:Illuminate\Pipeline\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
#34 Illuminate\Routing\Pipeline:Illuminate\Routing\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/AddQueuedCookiesToResponse.php:37
#33 Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse:handle in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
#32 Illuminate\Pipeline\Pipeline:Illuminate\Pipeline\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
#31 Illuminate\Routing\Pipeline:Illuminate\Routing\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php:59
#30 Illuminate\Cookie\Middleware\EncryptCookies:handle in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
#29 Illuminate\Pipeline\Pipeline:Illuminate\Pipeline\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
#28 Illuminate\Routing\Pipeline:Illuminate\Routing\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:104
#27 Illuminate\Pipeline\Pipeline:then in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Router.php:667
#26 Illuminate\Routing\Router:runRouteWithinStack in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Router.php:642
#25 Illuminate\Routing\Router:runRoute in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Router.php:608
#24 Illuminate\Routing\Router:dispatchToRoute in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Router.php:597
#23 Illuminate\Routing\Router:dispatch in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:176
#22 Illuminate\Foundation\Http\Kernel:Illuminate\Foundation\Http\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:30
#21 Illuminate\Routing\Pipeline:Illuminate\Routing\{closure} in /home/vagrant/code/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php:65
#20 Barryvdh\Debugbar\Middleware\InjectDebugbar:handle in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
#19 Illuminate\Pipeline\Pipeline:Illuminate\Pipeline\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
#18 Illuminate\Routing\Pipeline:Illuminate\Routing\{closure} in /home/vagrant/code/vendor/fideloper/proxy/src/TrustProxies.php:57
#17 Fideloper\Proxy\TrustProxies:handle in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
#16 Illuminate\Pipeline\Pipeline:Illuminate\Pipeline\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
#15 Illuminate\Routing\Pipeline:Illuminate\Routing\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php:31
#14 Illuminate\Foundation\Http\Middleware\TransformsRequest:handle in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
#13 Illuminate\Pipeline\Pipeline:Illuminate\Pipeline\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
#12 Illuminate\Routing\Pipeline:Illuminate\Routing\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php:31
#11 Illuminate\Foundation\Http\Middleware\TransformsRequest:handle in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
#10 Illuminate\Pipeline\Pipeline:Illuminate\Pipeline\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
#9 Illuminate\Routing\Pipeline:Illuminate\Routing\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php:27
#8 Illuminate\Foundation\Http\Middleware\ValidatePostSize:handle in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
#7 Illuminate\Pipeline\Pipeline:Illuminate\Pipeline\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
#6 Illuminate\Routing\Pipeline:Illuminate\Routing\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php:51
#5 Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode:handle in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:151
#4 Illuminate\Pipeline\Pipeline:Illuminate\Pipeline\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php:53
#3 Illuminate\Routing\Pipeline:Illuminate\Routing\{closure} in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:104
#2 Illuminate\Pipeline\Pipeline:then in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:151
#1 Illuminate\Foundation\Http\Kernel:sendRequestThroughRouter in /home/vagrant/code/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:116
#0 Illuminate\Foundation\Http\Kernel:handle in /home/vagrant/code/public/index.php:55

bobbybouwmann

Show your code!

Cronix
Cronix
1 month ago (571,210 XP)

Yes, as I mentioned, it need to be a relationship.

https://laravel.com/docs/5.6/eloquent-relationships

rema96

What part would you like to see? This is my personal project so I can share anything.

Here is my User modal for a starter:

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Cashier\Billable;


class User extends Authenticatable
{
    use Billable;
    use Notifiable;
    
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $with = ['subscription.plan.features'];

    public function isAdmin()
    {
        return $this->admin;
    }

//    public function features()
//    {
//        return User::join('subscriptions', 'users.id', '=', 'subscriptions.user_id')
//            ->join('plans', 'plans.id', '=', 'subscriptions.stripe_plan')
//            ->join('feature_plan', 'feature_plan.plan_id', '=', 'plans.id')
//            ->join('features', 'features.id', '=', 'feature_plan.feature_id')
//            ->where('users.id', '=', $this->id)
//            ->get();
//    }

    public function hasFeature($feature_id)
    {
        return $this->features()
                    ->where('slug', '=', $feature_id)
                    ->isNotEmpty();
    }

    public function subscriptionName()
    {
        if ($this->subscribed()) {
            $currPlanID = $this->subscription()->stripe_plan;
            $plan = Plan::where('id', '=', $currPlanID)->first();

            return $plan->name;
        }
        return null;
    }
}

rema96

Please not this is Billable as I said it's using cashier so subscription relationship is implemented in Billable.php

/**
     * Get a subscription instance by name.
     *
     * @param  string  $subscription
     * @return \Laravel\Cashier\Subscription|null
     */
    public function subscription($subscription = 'default')
    {
        return $this->subscriptions->sortByDesc(function ($value) {
            return $value->created_at->getTimestamp();
        })
        ->first(function ($value) use ($subscription) {
            return $value->name === $subscription;
        });
    }

    /**
     * Get all of the subscriptions for the Stripe model.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function subscriptions()
    {
        return $this->hasMany(Subscription::class, $this->getForeignKey())->orderBy('created_at', 'desc');
    }
Cronix
Cronix
1 month ago (571,210 XP)
public function subscription($subscription = 'default')
    {
        return $this->subscriptions->sortByDesc(function ($value) {
            return $value->created_at->getTimestamp();
        })
        ->first(function ($value) use ($subscription) {
            return $value->name === $subscription;
        });
    }

thats not a relationship though. A relationship has return $this->hasMany(), return $this->belongsToMany(), etc. It returns a relationship instance. subscriptions, is a relationship.

bobbybouwmann

Well it returns a single subscription, so that is correct! However with only works with full relationships.

You can create a relationship which returns a hasOne instead of hasMany and you should be good to go!

rema96

I gave it a bit of thought and solved it.

As Cronix said, subscription() isn't returning a relationship. Therefore I made my own function sub() which returns a relationship, then I user $with attribute. protected $with = ['sub.plan.features'];

public function sub()
    {
        return $this->hasOne('App\Subscription');
    }

Thank you both for helping!

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