Eloquent relationship where not in sub-query - php

I have got a relationship where I select all row's based on the category, however I need to exclude some of these if they are within a sub query.
/** #var \Illuminate\Database\Query\Builder $images */
$images = $vehicle->images()
->whereIn('image_category', $website->image_categories)
->orderBy('seq', 'ASC');
$images->whereNotIn('id', static function ($q) {
return $q->select('id')
->whereIn('image_category', [0, 99])
->groupBy('seq')
->having(DB::raw('count(`seq`)'), '>', 1);
});
dd($images->toSql(), $images->getBindings());
So above is my code, nearly works as I want it, however it seems that the $q variable doesn't have the table name within the query, below is the query outputted:
select
*
from
`vehicle_images`
where
`vehicle_images`.`vehicle_id` = ?
and `vehicle_images`.`vehicle_id` is not null
and `image_category` in (?, ?)
and `id` not in (
select
`id`
where
`image_category` in (?, ?)
group by
`seq`
having
count(`seq`) > ?
)
order by
`seq` asc
This is the relationship:
public function images()
{
return $this->hasMany(VehicleImage::class);
}

You can specify what table you want to use.
$images->whereNotIn('id', static function ($q) {
return $q->select('id')->from('{CORRECT_TABLE_NAME_HERE}')
->whereIn('image_category', [0, 99])
->groupBy('seq')
->having(DB::raw('count(`seq`)'), '>', 1);
});
I don't know what exactly the table name should be, hence the placeholder.

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)

Eloquent add optional condition to hasMany where foreign key is null

How do you add an optional/OR condition to a eloquent relationship?
E.g I want all the users comments OR where the foreign key (user_id) is NULL.
select * from `comments` where (`user_id` is null OR `comments`.`user_id` in (1, 2, 3)) AND `status` = 1
In the User relationship added orWhereNull
public function comments() {
return $this->hasMany(Comments::class)->orWhereNull('user_id');
}
But Laravel it's running:
select * from `comments` where `user_id` is null and `comments`.`user_id` in (1, 2, 3)
Surprised this hasn't been asked before only thing I found similar was this:
https://laracasts.com/discuss/channels/eloquent/eloquent-orwherenull-when-where-has-no-results
I tried this but it needs the model not the query builder.
return $this->where(function ($query){
return $query::hasMany(Comment::class)->orWhereNull('user_id');
});
I'm using eager loading to fetch the comments for a list of users.
$users = User::with('comments')->where('active', 1)->paginate(10);
It doesn't work because the "orWhere" is sent to the underlying query builder immediately but the foreign key constraint is added when the query is run. I couldn't work out a nice way to sort that but this workaround is fine for my use case where I'm only selecting one row at a time with this relation (granted I should probably use replace it with a getter...):
(Excuse any mistakes, changing model names for clarity):
class Customer
{
public function selectedOrCurrentWeek(): HasOne
{
return $this->hasOne(Week::class, 'id', 'selected_week_id')
->withDefault(function (Week $instance, Customer $parent) {
return $instance->newQuery()
->whereRaw('CURRENT_TIMESTAMP between start_date and end_date')
->where('customer_id', $parent->id)
->first();
});
}
Query log when fetching a customer by ID :-
select * from `customers` where
`customers`.`id` = ?
and `customers`.`deleted_at` is null
limit 1;
select * from `weeks` where
CURRENT_TIMESTAMP between start_date and end_date
and `customer_id` = ?
and `weeks`.`deleted_at` is null
limit 1;
but it will run the second query once per row
You can optimize this further to your need, just giving an idea on query
$users = User::with('comments', function($query){
$query->where('user_id', '=', null)->where('status', '1');
})->paginate(10);

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.

I'm trying to load only the last 3 comments on every post

i want get all posts with last three comment on each post. my relation is
public function comments()
{
return $this->hasMany('App\Commentpostfeed','post_id')->take(3);
}
This would return only 3 comments total whenever I called it instead of 3 comments per post.
i use this way :
1 :
Postfeed::with(['comment' => function($query) {
$query->orderBy('created_at', 'desc')->take(3); }]);
2 :
$postings = Postfeed::with('comments')->get();
but getting same result. please help me out for this problem.
Can you try like that ?;
Postfeed::with('comment')->orderBy('id','desc')->take(3);
Using plain mysql (If using Mysql) query you can get 3 recent comments per post using following query which rejoins comment table by matching created_at
SELECT p.*,c.*
FROM posts p
JOIN comments c ON p.`id` = c.`post_id`
LEFT JOIN comments c1 ON c.`post_id` = c1.`post_id` AND c.`created_at` <= c1.`created_at`
GROUP BY p.`id`,c.`id`
HAVING COUNT(*) <=3
ORDER BY p.`id`,c.`created_at` DESC
Sample Demo
Using laravel's query builder you can write similar to
$posts = DB::table('posts as p')
->select('p.*,c.*')
->join('comments c', 'p.id', '=', 'c.post_id')
->leftJoin('comments as c1', function ($join) {
$join->on('c.post_id', '=', 'c1.post_id')->where('c.created_at', '<=', 'c1.created_at');
})
->groupBy('p.id')
->groupBy('c.id')
->having('COUNT(*)', '<=', 3)
->orderBy('p.id', 'asc')
->orderBy('c.created_at', 'desc')
->get();
You can create a scope in the BaseModel like this :
<?php
class BaseModel extends \Eloquent {
/**
* query scope nPerGroup
*
* #return void
*/
public function scopeNPerGroup($query, $group, $n = 10)
{
// queried table
$table = ($this->getTable());
// initialize MySQL variables inline
$query->from( DB::raw("(SELECT #rank:=0, #group:=0) as vars, {$table}") );
// if no columns already selected, let's select *
if ( ! $query->getQuery()->columns)
{
$query->select("{$table}.*");
}
// make sure column aliases are unique
$groupAlias = 'group_'.md5(time());
$rankAlias = 'rank_'.md5(time());
// apply mysql variables
$query->addSelect(DB::raw(
"#rank := IF(#group = {$group}, #rank+1, 1) as {$rankAlias}, #group := {$group} as {$groupAlias}"
));
// make sure first order clause is the group order
$query->getQuery()->orders = (array) $query->getQuery()->orders;
array_unshift($query->getQuery()->orders, ['column' => $group, 'direction' => 'asc']);
// prepare subquery
$subQuery = $query->toSql();
// prepare new main base Query\Builder
$newBase = $this->newQuery()
->from(DB::raw("({$subQuery}) as {$table}"))
->mergeBindings($query->getQuery())
->where($rankAlias, '<=', $n)
->getQuery();
// replace underlying builder to get rid of previous clauses
$query->setQuery($newBase);
}
}
And in the Postfeed Model :
<?php
class Postfeed extends BaseModel {
/**
* Get latest 3 comments from hasMany relation.
*
* #return Illuminate\Database\Eloquent\Relations\HasMany
*/
public function latestComments()
{
return $this->comments()->latest()->nPerGroup('post_id', 3);
}
/**
* Postfeed has many Commentpostfeeds
*
* #return Illuminate\Database\Eloquent\Relations\HasMany
*/
public function comments()
{
return $this->hasMany('App\Commentpostfeed','post_id');
}
}
And to get the posts with the latest comments :
$posts = Postfeed::with('latestComments')->get();
Ps :
Source
For many to many relationships
You can do it like this,
Postfeed::with('comments',function($query){
$query->orderBy('created_at', 'desc')->take(3);
})
->get();

Laravel Custom Query

I have this query
SELECT ANY_VALUE(id) as id, title FROM `major` where university_id=1 group BY `title` order by id asc
I want to convert it into Laravel Query , I have a model majors and the function as follow
public static function retrieveByUniversityIDForWeb($universityId){
return self::select(DB::raw('ANY_VALUE(id) as id, title'))->from('major')->where('university_id', $universityId)->orderBy('id','desc')->simplePaginate(6);
}
but its not returning me the results , query works in phpmyadmin. Any idea what I missed?
You're declaring the method on your model which references its table by default. Also there's no need for using ANY_VALUE in your query. If you need it for some reason then you can change the select below to selectRaw('ANY_VALUE(id) as id, title')
public static function retrieveByUniversityIDForWeb($universityId)
{
return self::select('id', 'title')
->where('university_id', $universityId)
->orderBy('id', 'desc')
->simplePaginate(6);
}

Categories