Eloquent add optional condition to hasMany where foreign key is null - php

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);

Related

How to do order by the eloquent query without using join relationship in laravel?

How to order laravel eloquent query using parent model?
I mean I have an eloquent query where I want to order the query by its parent without using join relationship?
I used whereHas and order by on it, but did not work.
Here is a sample of my code:
$query = Post::whereHas('users')->orderBy('users.created_at')->get();
If you want to order Post by a column in user you have to do a join in some way unless you sort after you retrieve the result so either:
$query = Post::select('posts.*')
->join('users', 'users.id', 'posts.user_id')
->orderBy('users.created_at')->get();
Note that whereHas is not needed anymore because the join (which is an inner join by default) will only result in posts that have a user.
Alternatively you can do:
$query = Post::has('users')
->with('users')
->get()
->sortBy(function ($post) { return $post->users->created_at; });
The reason is that eloquent relationships are queried in a separate query from the one that gets the parent model so you can't use relationship columns during that query.
I have no clue why you wanted to order Posts based on their User's created_at field. Perhaps, a different angle to the problem is needed - like accessing the Post from User instead.
That being said, an orderBy() can accept a closure as parameter which will create a subquery then, you can pair it with whereRaw() to somewhat circumvent Eloquent and QueryBuilder limitation*.
Post::orderBy(function($q) {
return $q->from('users')
->whereRaw('`users`.id = `posts`.id')
->select('created_at');
})
->get();
It should generate the following query:
select *
from `posts`
order by (
select `created_at`
from `users`
where `users`.id = `posts`.id
) asc
A join might serve you better, but there are many ways to build queries.
*As far as I know, the subquery can't be made to be aware of the parent query fields
You can simply orderBy in your Post model.
public function users(){
return $this->belongsTo(User::class, "user_id")->orderByDesc('created_at');
}
I hope this helps you.
You can try
Post::query()
->has('users')
->orderBy(
User::select('created_at')
->whereColumn('id', 'posts.user_id')
->orderBy('created_at')
)
->get();
The sql generated would be like
select * from `posts`
where exists (select * from `users` where `posts`.`user_id` = `users`.`id`)
order by (select `created_at` from `users` where `id` = `posts`.`user_id` order by `created_at` asc) asc
But I guess join would be a simpler approach for this use case.
Laravel Docs - Eloquent - Subquery Ordering

how to query whereHas on a belongs to many relationship using laravel

hi i am having a User and Task model and they have a many to many relation ship like below i added :
public function users()
{
return $this->belongsToMany(User::class,'user_tasks');
}
and in my user model :
public function tasks()
{
return $this->hasMany(Task::class);
}
and in my controller i want to get the task that are assigned to the logged in user like below :
$task = Task::whereHas('users', function(Builder $query){
$query->where('id',Auth::user()->id);
})->get();
dd($task);
but i get this error :
SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'id' in where clause is ambiguous (SQL: select * from `tasks` where exists (select * from `users` inner join `user_tasks` on `users`.`id` = `user_tasks`.`user_id` where `tasks`.`id` = `user_tasks`.`task_id` and `id` = 4))
and when i change the id to users.id i get empty value but when i load it like below :
$task = Task::with('users')->get();
i get all the task with the relationships and they are working well but with whereHas its not working
thanks in advance
Since you are dealing with the pivot table with this relationship you can use the pivot table field user_id to filter:
$task = Task::whereHas('users', function (Builder $query) {
$query->where('user_id', Auth::user()->id);
})->get();
why not just:
$userTasks=Auth::user()->tasks;
this will get the current user tasks.

Laravel - whereHas last relation is = 'manual'

I have a model Lead which 'belongsToMany' Status through lead_has_status table.
What I want is to get all the Leads where the last (current) status of that lead is 'manual'.
I've tried this
$leads = Lead::whereHas('statuses', function ($query) use ($q) {
$query->where('description', 'LIKE', '%' . $q . '%');
})->get();
But since the Lead still has it's old statuses linked to it, it returns every Lead that at some point had Status 'manual'. But I only want the Leads where current status is 'manual'. So I want to do something like
$leads = Lead::whereLast('statuses', function ($query) use ($q) {
$query->where('description', 'LIKE', '%' . $q . '%');
})->get();
Or
$leads = Lead::whereLast('statuses', function ($query) use ($q) {
$query->latest()->first()->where('description', 'LIKE', '%' . $q . '%');
})->get();
(I know this it not the way to do it, it's simply to express what I'm trying to do)
I hope the question makes sense, I am super confused right now.
AFAIK there is no built-in eloquent function that gives you this kind of behaviour. If you would like to solve this with SQL only, you would have to look at the other answer using SQL function MAX in a sub-query and add raw queries in your Laravel code (probably a little bit more efficient then rejecting non-matching).
However you can filter your collection after retrieval and reject the elements that doesn't match your criteria.
Assuming the variable $q is of type string and contains a status e.g. "manual", something like this should do the job (code not tested).
$leads = Lead::whereHas('statuses', function ($query) use ($q) {
$query->where('description', 'LIKE', '%' . $q . '%');
})
->with('statuses')
->get()
->reject(function($element) use($q) {
$lastStatus = $element->statuses[count($element->statuses)-1];
return (strpos($lastStatus->description, $q) === false);
});
If you're column description should contain the exact string "manual" I would do it this way instead:
$leads = Lead::whereHas('statuses', function ($query) {
$query->where('description', 'manual');
})
->with('statuses')
->get()
->reject(function($element) {
$lastStatus = $element->statuses[count($element->statuses)-1];
return $lastStatus->description !== 'manual';
});
The whereHas method will not remove any relations like you need. The whereHas method will only say "select the records that have one or more children that matches this criteria", however all of the children records will be accessible, even the once that doesn't matches your criteria, as long as the parent has one that matches.
What you would like to say is "select the records that have one or more children that matches this criteria (whereHas) THEN remove all parents where the last child record does not match the record I am looking for (reject)"
Edit: Updated code using ->with('statuses') for improved performance.
It looks like you already have a great Laravel answer, but in case you end up going down the native query option, which you can then hydrate, here's a raw sql option... At the least, I hope it might help you visualise what you're trying to achieve.
CREATE TABLE leads (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`info` varchar(100) NULL default NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE statuses (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`leads_id` int(11) null default null,
`description` varchar(100) NULL default NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `leads` (`info`) VALUES ('foo'), ('bar'), ('test');
INSERT INTO `statuses` (`leads_id`, `description`) VALUES
(1, 'manual'),
(1, 'auto'),
(1, 'auto'),
(2, 'auto'),
(2, 'auto'),
(2, 'manual'),
(3, 'manual'),
(3, 'manual'),
(3, 'manual');
SELECT l.*
FROM statuses s
JOIN leads l ON l.id = s.leads_id
WHERE s.id IN (
SELECT MAX(s.id)
FROM leads l
JOIN statuses s ON s.leads_id = l.id
GROUP BY l.id
)
AND s.description LIKE '%manual%'
My solution is to define a new relationship on the Lead table.
class Lead extends Model
{
public function lastStatus()
{
return $this->statuses()->latest()->limit(1);
}
}
This way, the final query can be a bit simpler.
$leads = Lead::whereHas(‘lastStatus’, function ($query) use ($q) {
$query->where(‘description’, ‘LIKE’, $q);
}

Laravel 5.8 Eloquent query without using DB::raw() -- get rows based on latest value of many to many relationship

My goal is to select all issues that have been marked as false-positive. The issues are connected to Status via a ManyToMany relationship with pivot table IssuesStatus. The current status of an issue is determined by the value of the status column on the Status table.
I've come up with a solution that works but seems somehow suspect. I'm looking for a way to rewrite the query using Eloquent query builder without relying on the DB::raw() method.
public function getFalsePositivesAttribute() {
return Issue::where(DB::raw(
'( select `status` '.
'from `issues-status` '.
'left join `status` on `status`.id = `issues-status`.status_id '.
'where `issues-status`.issue_id = `issues`.id '.
'order by `issues-status`.id desc limit 1 )'
), '=', 'false-positive')->get();
}
Example of desired SQL query:
SELECT
`Issues`.id
FROM
`issues` AS `Issues`
LEFT JOIN
`issues-status` `IssueStatus` on `Issues`.id = `IssueStatus`.issue_id
LEFT JOIN
`status` as `StatusTable` on `IssueStatus`.status_id = `StatusTable`.id
WHERE
`Issues`.report_id = 2
AND
(
SELECT
`status`
FROM
`issues-status` `IssueStatus`
LEFT JOIN
`status` `StatusTable` on `StatusTable`.id = `IssueStatus`.status_id
WHERE
`IssueStatus`.issue_id = `Issues`.id
ORDER BY
`IssueStatus`.id desc
LIMIT 1
) = 'false-positive'
GROUP BY
`Issues`.id
Models:
class Issue extends Model {
...
public function status() {
return $this->belongsToMany( Status::class, 'issues-status')
->withTimestamps()
->withPivot('note');
}
...
}
class Status extends Model {
...
public function issues() {
return $this->hasMany(Issue::class);
}
...
}
Tables:
Issues:
id - identity
Status
id - identity
status - string
IssueStatus
id - identity
issue_id - relation to Issues
status_id - relation to Status
created_at - timestamp
note - text
If i understood correctly, you want to fetch the issue where the latest status is equals to something.
Add a latestStatus function to your Issues model:
public function latestStatus()
{
return $this->hasMany(IssueStatus::class)->latest();
}
Now, swap your status function with this one:
public function status() {
return $this->belongsToMany( Status::class, 'issues-status')
->withTimestamps()
->withPivot('note');
}
Then:
//Get all issues which has the desired status
Issue::whereHas('status', function($query){
$query->where('id', $desiredStatusId);
})
->get()
->reject(function($issue){
//filter the collection by the latest issue status,
// and reject those who does not have the desired latest status;
return $issue->latestStatus()->first()->status_id != $desiredStatusId;
});
Hope it helps.

Order By before Group By using Eloquent (Laravel)

I have a "messages" table with the following columns
CREATE TABLE `messages` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`fromId` int(11) NOT NULL,
`toId` int(11) NOT NULL,
`message` text NOT NULL,
`status` int(11) NOT NULL,
`device` varchar(100) NOT NULL,
`createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=57 DEFAULT CHARSET=latin1;
I'm trying to get all messages where 'toId' = $id and grouping by fromId. The problem is that the "message" shown on the results is the first ones, not the latest ones. I tried ordering by createdAt but it's not working.
How can I order by "createdAt" prior to querying and grouping the results? I want to do this in the laravel way using Eloquent.
My query:
$chats = Message::with('sender','recipient')
->where('toId',$id)
->orderBy('createdAt')
->groupBy('fromId')
->paginate(10)
I just needed to do something similar with a messages model. What worked for me was applying the unique method on the returned eloquent collection.
Model::where('toId', $id)
->orderBy('createdAt', 'desc')
->get()
->unique('fromId');
The query will return all messages ordered by createdAt and the unique method will reduce it down to one message for each fromId. This is obviously not as performant as using the database directly, but in my case I have further restrictions on the query.
Also, there are many more useful methods for working with these collections: https://laravel.com/docs/5.2/collections#available-methods
I found a way to do that! Basically, creating a subquery and running it before, so that results are ordered as expected and grouped after.
Here is the code:
$sub = Message::orderBy('createdAt','DESC');
$chats = DB::table(DB::raw("({$sub->toSql()}) as sub"))
->where('toId',$id)
->groupBy('fromId')
->get();
Try this query :
$chats = Message::with('sender','recipient')
->where('toId',$id)
->whereRaw('id IN (select MAX(id) FROM messages GROUP BY fromId)')
->orderBy('createdAt','desc')
->paginate(10)
It should be something along this:
Message::whereToId($id)->groupBy('fromId')->latest('createdAt')->first();
Update
After seeing the query that you've added, you probably just need to add a direction to the orderBy function, like this:
$chats = Message::with('sender','recipient')
->select(DB::raw('*, max(createdAt) as createdAt'))
->where('toId',$id)
->orderBy('createdAt', 'desc')
->groupBy('fromId')
->paginate(10)
This work for me:
(you can remove the order by createdAt in case createAt increase along with id)
DB::select(DB::raw('select * from (select * from messages group by fromId desc) m order by m.createdAt'));
$data = MCCAddMilkEntry::where('coming_from','=','society')
->whereRaw('id = (select max(`id`) from mcc_add_milk_entry)')->get();
#Sergio your answer is good but what if someone wanted to select a related model along with....?
Answer : I have tested every single function but I didn't fetch the correct data.
unique() gives data with associated array.
groupBy() gives better but picking first message.
Solution for Latest inbox messages. My case:
$data = ChatMessage::where('incoming_msg_id', auth()->user()->id)->orWhere('outgoing_msg_id', auth()->user()->id)->latest('msg_id')->with('messageable')->get();
$inbox = [];
$itemIds = [];
foreach ($data as $key => $value) {
if (!in_array($value['messageable_id'].$value['msg_for'], $itemIds)) {
array_push($inbox,$value);
array_push($itemIds,$value['messageable_id'].$value['msg_for']);
}
}
return $this->sendResponse($inbox,'Inbox');
Your case:
$chats = Message::where('toId',$id)->orWhere('fromId', $id)->latest('id')->with('sender','recipient')->get();
$inbox = [];
$fromIds = [];
foreach ($chats as $key => $value) {
if (!in_array($value['fromId'], $fromIds)) {
array_push($inbox,$value);
array_push($fromIds,$value['fromId']);
}
}
return $this->sendResponse($inbox,'Inbox');

Categories