Multiple Joins in Eloquent from local scopes - php

Got a question regarding Eloquent and the scope functionality:
Assuming two scopes:
class Result extends Model {
public function scopeIsRace($query) {
return $query
->join('sessions', 'sessions.id', '=', 'results.session_id')
->where('sessions.type', 10)
}
public function scopeIsOfficial($query) {
return $query
->join('sessions', 'sessions.id', '=', 'results.session_id')
->join('events', 'events.id', '=', 'sessions.event_id')
->where('events.regular_event', 1);
}
}
Calling both of them performs two joins of sessions and the resulting query looks sth like this (doesnt work)
select * from `results` inner join `sessions` on `sessions`.`id` = `results`.`session_id` inner join `sessions` on `sessions`.`id` = `results`.`session_id` inner join `events` on `events`.`id` = `sessions`.`event_id` where `driver_id` = 24 and (`sessions`.`type` = 10 or `sessions`.`type` = 11) and `events`.`regular_event` = 1
How do I prevent the double join on sessions?

Thank you so much #Nima. Totally forget about an advanced whereHas. Used a structure llke this from your suggested question and it works perfectly fine:
public function scopeIsRace($query) {
return $query->whereHas('session', function($query){
$query->where('type', 10);
});
}
public function scopeIsOfficial($query) {
return $query->whereHas('session', function($query) {
return $query->whereHas('event', function($query2) {
$query2->where('regular_event', 1);
});
});
}

Related

Laravel latest not working (not appearing in my SQL query)

So I have a Student model with this function:
public function latestStatus()
{
return $this->hasOne(StatusStudent::class)->latest();
}
then I just do a query with this latestStatus()
$query = Student::findOrFail(1);
$query = $query->whereHas('latestStatus', function($query) use ($statusuri) {
$query->where('status_id', 1);
});
dd($query->toSql());
and the toSql() function returns:
"select * from `students` where exists (select * from `status_student` where `students`.`id` = `status_student`.`student_id` and `status_id` = ?)
as if latest() is ignored.
Why doesn't latest() add anything to the query?
Thanks.
Edit:
I tried adding selectRaw for example:
public function latestStatus()
{
return $this->hasOne(StatusStudent::class)->selectRaw('MAX(status_student.id)');
}
and still nothing appears in my query.
If you dig deeper to the whereHas() relationship. It calls the has() method then if you look for the has() method you will see the getRelationWithoutConstraints() method, means that it will call the relationship but it will remove all the constraints attach to it and will only call the base query instance :
public function latestStatus()
{
return $this->hasOne(StatusStudent::class)->latest(); // the latest() will be removed in the query if you call the `latestStatus` using the `whereHas() or has()`
}
so if you use the whereHas() like the way you use it :
"select * from `students` where exists (select * from `status_student` where `students`.`id` = `status_student`.`student_id` and `status_id` = ?)
it will return the query with out the latest().
Instead of doing it like that you can do it like :
Student Model
public function status() : HasOne
{
return $this->hasOne(StatusStudent::class);
}
Controller
$student = Student::findOrFail(1);
$student->whereHas('status', function($query) {
$query->where('status_id', 1)
->latest();
})
But since the relationship is define as one-to-one :
$student = Student::findOrFail(1);
$student->load('status');
or
$student = Student::findOrFail(1)->status()->get();
Maybe you want to get the latest of all the status.
StudentStatus::query()->latest()->get();
As stated in a comment by #matticustard,
findOrFail() returns a model, not a query builder.
Instead of findOrFail(1) use where('id', 1)

whereHas vs join -> applying global scopes

I have a query with a few joins and global scopes for each model, for example:
SELECT *
FROM products p
WHERE EXISTS(
SELECT *
FROM orders o
WHERE o.user_id = 4
AND o.status_id = 1
AND o.user_id = 3
AND EXISTS(
SELECT *
FROM suborders s
WHERE s.status_id = 2
)
);
This means that I can simply write a few whereHas statements and my query will have some nested EXIST clauses, but all global scopes (like the user_id on the orders table) will be applied automatically:
$this->builder->whereHas('orders', function ($q) {
$q->where('status_id', '=', 1)
->whereHas('suborder', function ($q) {
$q->where('status_id', '=', 2);
});
});
The problem is that it's slow, it would be much better to have something with plain JOINs instead of ugly nested EXIST clauses:
SELECT *
FROM products p
INNER JOIN orders o ON p.order_id = o.id
INNER JOIN suborders s ON o.id = s.order_id
WHERE o.status_id = 1
AND u.user_id = 3
AND s.status_id = 2;
The problem with this is that I need to use query builder to join these:
$this->builder->join('orders', 'products.order_id', '=', 'orders.id')
->join('suborders', 'orders.id', '=', 'suborders.order_id')
->where('orders.status_id', 1)
->where('suborders.id', 2);
And that will not include any of my global scopes on Order and Suborder model. I need to do it manually:
$this->builder->join('orders', 'products.order_id', '=', 'orders.id')
->join('suborders', 'orders.id', '=', 'suborders.order_id')
->where('orders.status_id', 1)
->where('suborders.id', 2)
->where('orders.user_id', 3);
It's bad, because I need to replicate my global scopes logic every time I write a query like this, while whereHas applies them automatically.
Is there a way to join a table, and have all global scopes from the joined model applied automatically?
I have worked with a similar issue before and come up with some approach.
First, lets define a macro for Illuminate\Database\Eloquent\Builder in a service provider:
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
class AppServiceProvider
{
public function boot()
{
Builder::macro('hasJoinedWith', function ($table) {
return collect(
$this->getQuery()->joins
)
->contains(function (JoinClause $joinClause) use ($table) {
return $joinClause->table === $table;
})
});
}
}
Then, lets define the scopes:
class Product extends Model
{
public function scopeOrderStatus($query, $orderStatusId)
{
if (! $query->hasJoinedWith('orders')) {
$query->join('orders', 'products.order_id', '=', 'orders.id');
}
return $query->where('orders.status_id', $orderStatusId)
}
public function scopeOrderUser($query, $userId)
{
if (! $query->hasJoinedWith('orders')) {
$query->join('orders', 'products.order_id', '=', 'orders.id');
}
return $query->where('orders.user_id', $userId)
}
public function scopeSubOrder($query, $subOrderId)
{
if (! $query->hasJoinedWith('orders')) {
$query->join('orders', 'products.order_id', '=', 'orders.id');
}
if (! $query->hasJoinedWith('suborders')) {
$query->join('suborders', 'orders.id', '=', 'suborders.order_id');
}
return $query->where('suborders.id', $subOrderId)
}
}
Finally, you use the scopes together:
Product::orderStatus(1)
->subOrder(2)
->orderUser(3)
// This is optional. There are possibly duplicate products.
->distinct()
->get();
This is the best approach that I can come up with, there may be better ones.
Actually, that what you describe can't be true (will provide proof below). EXISTS don't make query slower and in 99% of case it make faster!
So laravel don't provide ability to make joins for relations from box.
I saw different solutions for this and package on github, but I will not provide link to it, because when I reviewed logic, found a lot of problems with field selecting and rare cases.
Laravel don't generate code with EXISTS as you describe, it add relation search by ID to each EXISTS subquery like
SELECT *
FROM products
WHERE EXISTS(
SELECT *
FROM orders
WHERE o.user_id = 4
AND o.status_id = 1
AND o.id = p.order_id
AND EXISTS(
SELECT *
FROM suborders s
WHERE s.status_id = 2
AND s.id = o.suborder_id
)
);
attention to AND o.id = p.order_id and AND s.id = o.suborder_id
When you do select with joins, you should exactly set SELECT from main table, to have right filled Model fields.
Global scopes are global. Purpose to be really global. If you have more then 1-2 places without them, then you should find another solution instead of global scopes. Otherwise you application will be very hard to support and write new code. Developer should not remember every time, that there can be global scopes that he should turn off

Eloquent: filter record based on relation

I'm trying to get customer specific users who don't have an owner role, but it also skips users who don't have any role. Users can have one or multiple roles. I want to get all users either having multiple roles or no role at all, but if the user contains an owner role then only that user should be ignored.
Note: I am using spatie/laravel-permission which gets users roles from model has roles intermediate table
Here is my scope query
public function scopeForCompany(EloquentBuilder $query, string $customerId): EloquentBuilder
{
$query->where(function (EloquentBuilder $q) {
$q->doesntHave('roles');
$q->orHas('roles');
});
$query->whereHas('roles', function (EloquentBuilder $q) {
$q->whereNotIn('name', ['owner']);
});
return $query->where('customer_id', $customerId);;
}
here is the test
public function it_apply_query_scope_to_get_customer_specific_users_only(): void
{
$model = new User;
// create non customer users
\factory(User::class, 2)->create();
$customer = \factory(Customer::class)->create();
foreach (['owner', 'admin', 'user'] as $role) {
$role = \factory(Role::class)->create(['name' => $role]);
$user = \factory(User::class)->create(['customer_id' => $customer->id]);
$user->roles()->save($role);
}
$scopedUsers = $model->newQuery()->forCompany($customer->id)->get();
$nonScopedUsers = $model->newQuery()->get();
static::assertCount(2, $scopedUsers); // Failed asserting that actual size 0 matches expected size 2.
static::assertCount(5, $nonScopedUsers);
}
Debug: here is the row query:
"select * from `users` where (not exists (select * from `roles` inner join `model_has_roles` on `roles`.`id` = `model_has_roles`.`role_id` where `users`.`id` = `model_has_roles`.`model_uuid` and `model_has_roles`.`model_type` = ?) or exists (select * from `roles` inner join `model_has_roles` on `roles`.`id` = `model_has_roles`.`role_id` where `users`.`id` = `model_has_roles`.`model_uuid` and `model_has_roles`.`model_type` = ?)) and exists (select * from `roles` inner join `model_has_roles` on `roles`.`id` = `model_has_roles`.`role_id` where `users`.`id` = `model_has_roles`.`model_uuid` and `model_has_roles`.`model_type` = ? and `name` not in (?)) and `customer_id` = ? and `users`.`deleted_at` is null"
This is what i tried first but didn't worked
return $query->whereHas('roles', function (EloquentBuilder $query): void {
$query->whereNotIn('name', ['owner']);
})->where('customer_id', $customerId);
Any help would be appreciated thanks
You need to do some Or logic for this to happen. I break the query up into 3 pieces.
The statement: "I want to get all users either having multiple roles"
$query->has('roles', '>=', 2);
Next you want all with no roles: "or no role at all".
$query->doesntHave('roles');
And lastly your query correctly filter out where the role cannot be the owner.
$query->whereHas('roles', function (EloquentBuilder $query): void {
$query->whereNotIn('name', ['owner']);
})
Putting it all together doing something like, with a sub where query. To proper do the Or logic you want.
$query->where(function($builder){
$builder->has('roles', '>=', 2);
$builder->whereHas('roles', function (EloquentBuilder $query): void {
$query->whereNotIn('name', ['owner']);
})
});
$builder->orDoesntHave('roles');
In pseudo logical statements this would look something similar to like:
(roles.each.name != 'owner' && count(roles) >= 2) || empty(roles)
Let's see if this help your case, else post the toSql() of the builder and let's figure it out. It's a fairly complex query builder logic this is doing.

subqueries in laravel 5.4

how can I make this query to query builder laravel 5.4?
select * from gb_employee where employee_id not in (select gb_emp_client_empid from gb_emp_client_lines where gb_emp_client_clientid =1) ;
Use a left join for this
$employee=DB::table('gb_employee as e')
->select('e.*')
->leftJoin('gb_emp_client_lines as el', 'e.employee_id', '=', 'el.gb_emp_client_empid')
->whereNull('el.gb_emp_client_empid',)
->get()
;
doing via eloquent way
class Employee extends Model
{
public function client_lines()
{
return $this->hasMany('App\ClientLines', 'gb_emp_client_empid', 'employee_id');
}
}
$employees = Employee::doesntHave('client_lines')->get();
I think I solved it. . .
$employees = DB::table('gb_employee')
->whereNotIn('employee_id', function($query) use ($client_id)
{
$query->select('gb_emp_client_empid')
->from('gb_emp_client_lines')
->where('gb_emp_client_clientid',$client_id);
})
->get();

Laravel Eloquent: filtering model by relation table

I have places and locations tables.
Place could have many locations. Location belongs to Place.
Place:
id
title
Location:
id
place_id
floor
lat
lon
class Location extends Model {
public function place()
{
return $this->belongsTo('App\Place');
}
}
And
class Place extends Model {
public function locations()
{
return $this->hasMany('App\Location');
}
}
And i need to find places, that belongs only to 1st floor. select * from places inner join locations on places.id = locations.place_id where locations.floor = 1
How does it should be done in Eloquent?
Is something similar to
Place::where('locations.floor', '=', 1)->get() exists?
Yes, i know there is whereHas:
Place::whereHas('locations', function($q)
{
$q->where('floor', '=', 1);
})->get()
but it generates a bit complex query with counts:
select * from `places` where (select count(*) from `locations` where `locations`.`place_id` = `places`.`id` and `floor` = '1') >= 1
does not this works?
class Location extends Model {
public function place()
{
return $this->belongsTo('App\Place');
}
}
$locations = Location::where('floor', '=', 1);
$locations->load('place'); //lazy eager loading to reduce queries number
$locations->each(function($location){
$place = $location->place
//this will run for each found location
});
finally, any orm is not for database usage optimization, and it is not worth to expect nice sql's produced by it.
I haven't tried this, but you have eager loading and you can have a condition:
$places = Place::with(['locations' => function($query)
{
$query->where('floor', '=', 1);
}])->get();
Source
Try this :
Place::join('locations', 'places.id', '=', 'locations.place_id')
->where('locations.floor', 1)
->select('places.*')
->get();

Categories