chinhle's avatar

filter many to many relation result by column in pivot table

I have Employee:

id
name

and Company:

id
name

and company_employee :

employee_id
company_id
employee_role

Now, I need to get all employees that belongs to a company under employee_role = $role. How to i do that ? Trying to do this :

$normalEmployees = Employee::with(['workingCompanies' => function ($query) use ($role) {
                $query->withPivot('employee_role')->wherePivot('employee_role',$role);
            }])->get();

gives me whole table ...

Thanks

0 likes
17 replies
Sergiu17's avatar
$normalEmployees = Employee::with(['workingCompanies' => function($query) use ($role) {
    $query->where('employee_role',  $role); 
}])->get();

Is this working?

devk's avatar

Everything is correct except you need to use whereHas instead of with:

$normalEmployees = Employee::whereHas('workingCompanies', function ($query) use ($role) {
    $query->withPivot('employee_role')->wherePivot('employee_role',$role);
})->get();
chinhle's avatar

Let me try out tomorrow and feedback. Thanks

Vilfago's avatar

For information, you filtered the relation in your first example.

So you took all employees, but for them, only a part of working companies.

And as you requested only one company, ypu should filter that too

$normalEmployees = Employee::whereHas('workingCompanies', function ($query) use ($role, $company) {
    $query->withPivot('employee_role')->wherePivot('employee_role',$role)
->wherePivot('company_id', $company);
})->get();

Really not sure that 'withPivot' is mandatory if you already declare the relation, but I'll let you try.

chinhle's avatar

for Sergiu17 answer, I got this :

SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'where `employee_role` = ?)) and active = 1 and outsider=0' at line 1 (SQL: select * from `employees` where (`workingCompanies` = (select * where `employee_role` = 6)) and active = 1 and outsider=0)

for devk answer, I got this:

Call to undefined method Illuminate\Database\Query\Builder::withPivot()

I think both are not the right solutions ... And of course Vilfago's answer is similar to devk.

chinhle's avatar

The funny thing is, when I try to debug, the query Laravel generated using my original solution is :

select `companies`.*, `company_employee`.`employee_id` as `pivot_employee_id`, `company_employee`.`company_id` as `pivot_company_id`, `company_employee`.`employee_role` as `pivot_employee_role` from `companies` inner join `company_employee` on `companies`.`id` = `company_employee`.`company_id` where `company_employee`.`employee_role` in ('4', '6', '7', '8', '9', '10', '11') and `company_employee`.`employee_id` in ('13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '35', '37', '38', '39', '41', '42', '43', '44', '45', '46', '47', '49', '50', '51', '53', '54', '55', '56', '57', '58', '59', '60', '61', '62', '63', '64', '65', '66', '68', '69', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '80', '81', '82', '83', '84', '85', '86') and `company_employee`.`employee_role` = '6'

I try running it in Sequel Pro, MySQL 5.6.35 , it DOES give the right results. Somehow in Laravel, it returns the whole table ... any idea why ??

chinhle's avatar

Even more weird, out of nowhere, there's a

select * from `employees` where active = 1 and outsider =0

generated before the above query. I did some tracing and it really boils down to this call

$normalEmployees = Employee::with(['workingCompanies' => function ($query) use ($role) {
                $query->withPivot('employee_role')->wherePivot('employee_role',$role);
            }])->get();

that generated the select * query ... totally lost .. This is my workingCompanies relation

public function workingCompanies($role = null) {
        $normalEmployeeRoles = $role ? [$role] : EmployeeRole::employeeRoles();
        return $this->belongsToMany(Company::class)->withPivot('employee_role')->wherePivotIn('employee_role', $normalEmployeeRoles);
    } 

Anyone has any idea ??

Vilfago's avatar

For Eloquent this query

$normalEmployees = Employee::with(['workingCompanies' => function ($query) use ($role) {
                $query->withPivot('employee_role')->wherePivot('employee_role',$role);
            }])->get();

is the same as (consider it as "pseudocode")

$employees = Employee::all();
$companies = WorkingCompany::join('employee_role', '[.. join condition ..]')->whereIn('employee_role.employee_role', $normalEmployeeRoles);

$normalEmployees = consolidate($employees, $companies);

so, it's normal you have 2 queries.

Have you tried :

$normalEmployees = Employee::whereHas('workingCompanies', function ($query) use ($role, $company) {
    $query->wherePivot('employee_role',$role)->wherePivot('company_id', $company);
})->get();

Or maybe, give us the relation you had in your models ?

chinhle's avatar

I'm not putting $company in because I want to get all employee of $employee_role no matter which company he's working at. Here's what I got running whereHas

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'pivot' in 'where clause' (SQL: select * from `employees` where exists (select * from `companies` inner join `company_employee` on `companies`.`id` = `company_employee`.`company_id` where `employees`.`id` = `company_employee`.`employee_id` and `pivot` = employee_role and `pivot` = company_id and `company_employee`.`employee_role` in (4, 6, 7, 8, 9, 10, 11)) and active = 1 and outsider =0)

I gave you the workingCompanies relation above, what else do you need ?

chinhle's avatar

Wait, you sure it's implemented this way ??

$employees = Employee::all();
$companies = WorkingCompany::join('employee_role', '[.. join condition ..]')->whereIn('employee_role.employee_role', $normalEmployeeRoles);

$normalEmployees = consolidate($employees, $companies);

Because if I'm right, what it does it basically returning the whole Employee table, and eagerload 'workingCompanies' where condition met ... no wonder I got the whole table back

Vilfago's avatar

Sorry, don't see that

public function workingCompanies($role = null) {
        $normalEmployeeRoles = $role ? [$role] : EmployeeRole::employeeRoles();
        return $this->belongsToMany(Company::class)->withPivot('employee_role')->wherePivotIn('employee_role', $normalEmployeeRoles);

When using with('workingCompanies') the attribute $roll will always be null.

Therefore, you load all roles with :

EmployeeRole::employeeRoles();

So change your relation to :

public function workingCompanies() {
        return $this->belongsToMany(Company::class);

and then

$normalEmployees = Employee::whereHas('workingCompanies', function ($query) use ($role, $company) {
    $query->wherePivot('employee_role',$role);
})->get();
chinhle's avatar
chinhle
OP
Best Answer
Level 1

I just tried, the right answer is

$normalEmployees = Employee::whereHas('workingCompanies', function ($query) use ($role) {
                $query->where('employee_role',$role);
            })->get();

We use 'where' instead of 'wherePivot'. Everything keeps the same for me. Thanks for your advice. I'm a newbie here, so, which answer do I mark correct ?

Vilfago's avatar

Yours, as it's the one that is correct ;)

aries@bangli.uk's avatar

Hi chinhle, one more question from my side, in your right answer

$query->where('employee_role',$role)

the column in this where clause is all the pivot table column names, isn't it? like if I want to filter with company id, I should use 'company_id'. Thanks

chinhle's avatar

In this specific case ? If u want to filter by company_id, I guess we should do something like Company::find($company_id)->employees->wherePivot(‘employee_role’,$role)

aries@bangli.uk's avatar

Hi Chinhle, Thanks, I am just wondering about the column name in the sub clause " $query->where('employee_role',$role);" in your code.

$normalEmployees = Employee::whereHas('workingCompanies', function ($query) use ($role) { $query->where('employee_role',$role); })->get();

the first parameter in where('employee_role',$role), should come from the pivot table as my understanding. I agree with your way to filter the employees belong to one company in your reply. But if I write like below, I could write

$normalEmployees = Employee::whereHas('workingCompanies', function ($query) use ($role) { $query->where('company_id', $id); // column for the pivot table })->get();

or

$normalEmployees = Employee::whereHas('workingCompanies', function ($query) use ($role) { $query->where('id',$id); //column name from company table })->get();

I tried both are working. Hope you agree with me.

jivanrij's avatar

Check out these examples from the Laravel 10 documentation.

https://laravel.com/docs/10.x/eloquent-relationships#filtering-queries-via-intermediate-table-columns

return $this->belongsToMany(Role::class)
                ->wherePivot('approved', 1);
 
return $this->belongsToMany(Role::class)
                ->wherePivotIn('priority', [1, 2]);
 
return $this->belongsToMany(Role::class)
                ->wherePivotNotIn('priority', [1, 2]);
 
return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
 
return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
 
return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotNull('expired_at');
 
return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotNotNull('expired_at');

Please or to participate in this conversation.