Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Add eager loading of pivot relations #37456

Closed
wants to merge 10 commits into from

Conversation

ajcastro
Copy link

@ajcastro ajcastro commented May 22, 2021

This PR provides a way for us to eager-load defined relations from a Pivot model.
Currently, we can define relations in pivot models and access them via lazy-loading, but cannot be eager-loaded.

For example in my payroll project, where an Employee can have many Deduction instances (many-to-many). But these deductions should also be set if which PayrollPeriod it is included.

class Employee extends Model 
{
    public function deductions()
    {
        return $this->belongsToMany(\App\Models\Deduction::class, 'employee_deduction')
            ->withTimestamps()
            ->withPivot([
                 // we make sure we included the payroll_period_id in the pivot attributes which we need to eager load later
                'payroll_period_id', 
            ])
            ->using(EmployeeDeduction::class)
            ->as('employeeDeduct');
    }
}
// We need to have a custom pivot model which is in our case it is EmployeeDeduction 
// Then this pivot model has the relation to payrollPeriod.
class EmployeeDeduction extends Pivot
{
    public $incrementing = true;

    public function payrollPeriod()
    {
        return $this->belongsTo(PayrollPeriod::class);
    }
}

Below are sample usages:
Lazy-loading, currently supported:

$employee = Employee::with('deductions')->find($id);
foreach ($employee->deductions as $deduction) {
   // this is lazy-loading the payrollPeriod relation in EmployeeDeduction  which is currently supported
   dump($deduction->pivot->payrollPeriod); 

   // or like this when using custom pivot accessor
   dump($deduction->employeeDeduct->payrollPeriod); 
}

But currently it cannot be eager loaded. But with this PR, we can achieve it like this:

Employee::with('deductions.pivot.payrollPeriod')->get(); 

// or like this when using custom pivot accessor
Employee::with('deductions.employeeDeduct.payrollPeriod')->get(); 

The BelongsToMany relation can now detect if you intend to eager load a relation from your custom pivot model by checking if the $accessor name exists in the $eagerLoad variable of query builder.

You can define as many relations in the Pivot model and eager-load them as many as you can as long as you make sure
to include their foreign keys in in the withPivot() declaration.

For example let's add a user relation in our EmployeeDeduction which means the user who created this record.

class EmployeeDeduction extends Pivot
{
    public $incrementing = true;

    public function payrollPeriod()
    {
        return $this->belongsTo(PayrollPeriod::class);
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
class Employee extends Model
{
    public function deductions()
    {
        return $this->belongsToMany(\App\Models\Deduction::class, 'employee_deduction')
            ->withTimestamps()
            ->withPivot([
                'payroll_period_id',
                'user_id', // we also include the user_id foreign key here
            ])
            ->using(EmployeeDeduction::class)
            ->as('employeeDeduct');
    }
}

Then we can eager load the payrollPeriod and the user relations like so:

Employee::with([
    'deductions.pivot.payrollPeriod',
    'deductions.pivot.user',
])->get();

// or if you define custom pivot accessor
Employee::with([
    'deductions.employeeDeduct.payrollPeriod',
    'deductions.employeeDeduct.user',
])->get();

This PR is also important in relevance with #37363 which requires us to access on eager loaded relations only.

I have an itch of solving this feature since in Laravel 5.* (laravel/ideas#175), but the setup there is different. I think this is the perfect timing for it.

I also have tested this manually on my local laravel 8 project.

@ajcastro ajcastro changed the title Add eager loading of pivot relations [8.x] Add eager loading of pivot relations May 23, 2021
@ajcastro ajcastro marked this pull request as ready for review May 23, 2021 02:08
@ajcastro
Copy link
Author

I also noticed that hydrated pivot models' $preventsLazyLoading property is not being set in respect to Model::preventsLazyLoading() so I included it also in this PR. That way we can prevent unwanted n+1 queries when accessing pivot relations.

@ajcastro
Copy link
Author

This also supports eager loading of nested pivot relations. Below is an example where it eager loads payrollPeriod relation from EmployeeDeduction pivot and then the tag relation from PayrollPeriodUser pivot. Given that the PayrollPeriod has a many-to-many relation with User.

Employee::with('deductions.pivot.payrollPeriod.users.pivot.tag')->get();

@taylorotwell
Copy link
Member

I don't want to add more maintenance burden to Eloquent at the moment unless the feature is really compelling. In this case, I would just make your pivot model a real model.

@ajcastro
Copy link
Author

ajcastro commented May 24, 2021

@taylorotwell Changing it to a real model would work, but that would be a different data structure and would require code rewrite on how it access the relation. Everytime a developer sees to add a third relation on the belongsToMany, he would be required to change the relation into hasMany Pivot model and rewrite existing code for it. While in eagerloading pivot relations, a developer can add more relations progressively without requiring a code rewrite on existing relation access which is a better developer experience. Just my 2cents tho.

@ajcastro
Copy link
Author

Maybe I will put this in my package for now. I have existing package which supports Laravel 6. I will create another version for the Laravel 8 support. https://github.com/ajcastro/eager-load-pivot-relations

@moisish
Copy link
Contributor

moisish commented Nov 29, 2021

@taylorotwell I think this needs to be revisit since changing the pivot model to a real model is a different data structure that may not make sense.

For example the important data is the belongsToMany model and not the pivot where if we change the pivot model to real model we are adding an intermediate model

@valh1996
Copy link

Indeed, this PR would be welcome.

Transforming the pivot into a real model is still not really possible in my case for example since, as already said, it will require a big refactoring on an already very large code base.

@shehi
Copy link
Contributor

shehi commented Mar 23, 2022

@taylorotwell I also think this really needs to be in the framework. Can you revisit this topic please?

@BrunoGGM
Copy link

@taylorotwell This could be very helpful, I am facing this problem. Could the application be reviewed again?

@samharvey44
Copy link

@taylorotwell We consistently have a use case for this, please revisit this topic.

@gajosadrian
Copy link

@taylorotwell bump

@ryanmortier
Copy link

@taylorotwell would like this revisited too please if possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants