Order By before Group By using Eloquent (Laravel) - php

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

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

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

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

Why the search query always use ID?

I'm trying to use ->find()
I want to do query lie this
Where IdBiodata = '$IdBiodata'
Then I try this way :
Route::get('biodata/{IdBiodata}', function ($IdBiodata) {
$databiodata = DB::table('Biodata')->find($IdBiodata);
if(count($databiodata)) {
return view('biodata/vew',compact('databiodata'));
}else{
echo "Data not Found";
}
});
when I run it . I face this
SQLSTATE[42S22]: [Microsoft][SQL Server Native Client 10.0][SQL
Server]Invalid column name 'id'. (SQL: select top 1 * from [Biodata]
where [id] = 1)
how can i fix it ? thanks in advance
My Table structure
CREATE TABLE [dbo].[Biodata](
[IdBiodata] [int] IDENTITY(1,1) NOT NULL,
[Nama] [varchar](max) NULL,
[Umur] [int] NULL,
CONSTRAINT [PK_Biodata] PRIMARY KEY CLUSTERED
(
[IdBiodata] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
your search query written using query builder not using Eloquent ORM.find() function Retrieving A Record By Primary Key and Primary Key always take column name as id untill and unless you can not defined externally.
So $databiodata = DB::table('Biodata')->find($IdBiodata); when you write this it's assume Primary key as id.
in Eloquent ORM you can change Primary Key
class Biodata extends Eloquent {
protected $primaryKey = 'IdBiodata';
}
Now, you can write
Biodata::find($IdBiodata);
if you don't wan to use Eloquent ORM so simply use other function rather than find() use where()
$databiodata = DB::table('Biodata')->where('IdBiodata',$IdBiodata)->get();
SO now Replace Your Code using below code
From this
$databiodata = DB::table('Biodata')->find($IdBiodata);
To
$databiodata = DB::table('Biodata')->where('IdBiodata',$IdBiodata)->get();
find() function always use the primary key .. you can use where() instead of find() if you're going to filter it by other field .. here's an example using your query
$databiodata = DB::table('Biodata')->where('IdBiodata',$IdBiodata)->get();

Sorting with custom query data in paginator sort CakePHP

My controller query is as below :
$built_arr = $this->User->query("SELECT u1.id,
CASE WHEN u1.role = 'CW' THEN u2.agency_name
WHEN u3.role = 'EU' THEN u2.agency_name ELSE u1.agency_name END AS agency
FROM users u1 LEFT JOIN users u2 ON (u1.parent = u2.id AND u2.role = 'A')
LEFT JOIN users u3 ON (u1.id = u3.parent AND u1.role = 'CW')
LEFT JOIN users u4 ON (u1.parent = u4.id AND u4.role = 'A')
WHERE u1.role = 'A' OR u1.role = 'CW'
GROUP BY u1.id");
And My code of array from this query is as below :
if (isset($built_arr) && !empty($built_arr)) {
foreach ($built_arr AS $key => $value) {
if (isset($value[0]['agency']) && !empty($value[0]['agency'])) {
$agency_arr[$value['u1']['id']] = $value[0]['agency'];
}
}
}
Now I have set this data to view like as below :
$this->set('agency_arr', $agency_arr);
Now this Array (agency_arr), I have used as data of columns of a table in view page.
Now I want to sort that column with this array data.
My view code as below :
<th><?php echo $this->Paginator->sort('?', __("Agency Name"), array('escape' => false)); ?></th>
Advice for the "?" sign.
What I have to write instead of "?", So I can sort with my array data.
My other column's data come from the paginate query so those are working fine.
I need this extra query for extra column, so need to sort by that column.
To the sort question:
Cake\View\Helper\PaginatorHelper::sort($key, $title = null, $options =[])
Parameters:
$key (string) – The name of the column that the recordset should be sorted.
$title (string) – Title for the link. If $title is null $key will be used for the title and will be generated by inflection.
$options (array) – Options for sorting link.
So i guess this is what you want: $paginator->sort('agency_name', 'Agency Name', array('escape' => false));
I would suggest to set up associations properly if you didnt already and use a find call with contains.
Since there are parents i guess its a HasAndBelongsToMany relation. (Children have multiple parents and parents multiple children)
CREATE TABLE `users` (
user_id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
role varchar(255),
);
CREATE TABLE `childrens_parents` (
childrens_parents_id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
parent_id int,
children_id int,
FOREIGN KEY (u1_id) REFERENCES users(user_id),
FOREIGN KEY (u2_id) REFERENCES users(user_id)
);
In your Model/UsersTable set up the association properly like described in
the cakephp book
(dont forget targetforeignkey+foreignkey)
Now you can do something like this:
$query = $this->Users->find()->where(['role'=>'A'])->orWhere(['role'=>'CW'])->contain([
'Children' => function ($q) {
return $q
->where(['Role =' => 'CW']);
}
])->contain([
'Parents' => function ($q) {
return $q
->where(['Role =' => 'A']);
}
])->group('user_id');
Now you can paginate over the resultset accessing all fields of the objects.
foreach($query as $row) {
debug($row);//see how nicely the resultarray is looking
echo($row->Parents[0]->user-id);// access related Entities
}
If its not HABTM but hasMany or some other type you can adapt it easily.
And I strongly recommend on holding in cake conventions !
Ressources (I recommend on reading the whole articles for proper cakephp work):
http://book.cakephp.org/3.0/en/orm/query-builder.html
http://book.cakephp.org/3.0/en/orm/associations.html

Categories