18

I have 3 models

  • User
  • Channel
  • Reply

model relations

  • user have belongsToMany('App\Channel');
  • channel have hasMany('App\Reply', 'channel_id', 'id')->oldest();

let's say i have 2 channels

  • channel-1
  • channel-2

channel-2 has latest replies than channel-1

now, i want to order the user's channel by its channel's current reply. just like some chat application. how can i order the user's channel just like this?

  • channel-2
  • channel-1

i already tried some codes. but nothing happen

// User Model
public function channels()
    {
        return $this->belongsToMany('App\Channel', 'channel_user')
                    ->withPivot('is_approved')
                    ->with(['replies'])
                    ->orderBy('replies.created_at'); // error
    
    }
// also
public function channels()
    {
        return $this->belongsToMany('App\Channel', 'channel_user')
                    ->withPivot('is_approved')
                    ->with(['replies' => function($qry) {
                        $qry->latest();
                    }]);
    }
// but i did not get the expected result

EDIT also, i tried this. yes i did get the expected result but it would not load all channel if there's no reply.

public function channels()
{
    return $this->belongsToMany('App\Channel')
                ->withPivot('is_approved')
                ->join('replies', 'replies.channel_id', '=', 'channels.id')
                ->groupBy('replies.channel_id')
                ->orderBy('replies.created_at', 'ASC');
}
miken32
  • 42,008
  • 16
  • 111
  • 154
Hitori
  • 569
  • 1
  • 5
  • 21

4 Answers4

18

According to my knowledge, eager load with method run 2nd query. That's why you can't achieve what you want with eager loading with method.

I think use join method in combination with relationship method is the solution. The following solution is fully tested and work well.

// In User Model
public function channels()
{
    return $this->belongsToMany('App\Channel', 'channel_user')
        ->withPivot('is_approved');
}

public function sortedChannels($orderBy)
{
    return $this->channels()
            ->join('replies', 'replies.channel_id', '=', 'channel.id')
            ->orderBy('replies.created_at', $orderBy)
            ->get();
}

Then you can call $user->sortedChannels('desc') to get the list of channels order by replies created_at attribute.

For condition like channels (which may or may not have replies), just use leftJoin method.

public function sortedChannels($orderBy)
    {
        return $this->channels()
                ->leftJoin('replies', 'channel.id', '=', 'replies.channel_id')
                ->orderBy('replies.created_at', $orderBy)
                ->get();
    }

Edit:

If you want to add groupBy method to the query, you have to pay special attention to your orderBy clause. Because in Sql nature, Group By clause run first before Order By clause. See detail this problem at this stackoverflow question.

So if you add groupBy method, you have to use orderByRaw method and should be implemented like the following.

return $this->channels()
                ->leftJoin('replies', 'channels.id', '=', 'replies.channel_id')
                ->groupBy(['channels.id'])
                ->orderByRaw('max(replies.created_at) desc')
                ->get();
Community
  • 1
  • 1
Steve.NayLinAung
  • 5,086
  • 2
  • 25
  • 49
  • why use join when we are already using eloquent relation, I'll suggest just use join and apply orderby – kapilpatwa93 Nov 28 '16 at 07:49
  • 2
    Because the joined table's fields will be on the same row so ordering is possible. – The Alpha Nov 28 '16 at 07:53
  • 1
    so i used join method like this. `return $this->belongsToMany('App\Channel', 'channel_user') ->withPivot('is_approved') ->join('replies', 'replies.channel_id', '=', 'channels.id') ->orderBy('replies.created_at', 'DESC');` but when specific channel has no reply. it would cause an error. – Hitori Nov 28 '16 at 08:16
  • @Hitori. I will comment under my own post cause I don't want to annoy other peoples' one. I've carefully analyze your problem. As first, your original question asked about to query the channels. But now you describe your problem about to query the replies. Please exactly describe the problem. Is it "to query channels" or "to query replies of each channels". – Steve.NayLinAung Nov 28 '16 at 12:20
  • @Steve.NayLinAung, i'm so sorry for the mislead. hmm. think of it like the facebook messages. i want to display all channels by its replies.created_at – Hitori Nov 29 '16 at 03:55
  • @Hitori. See my updated answer and accept correct if it give you the solution. – Steve.NayLinAung Nov 29 '16 at 07:53
  • The solution at the bottom works. But if you use the top proposed solution then `sortedChannels` will give you duplicate channels for each reply (because of the join). So that didnt work for me, but the latter is good. – Adam May 24 '19 at 18:53
4

Inside your channel class you need to create this hasOne relation (you channel hasMany replies, but it hasOne latest reply):

public function latestReply()
{
    return $this->hasOne(\App\Reply)->latest();
}

You can now get all channels ordered by latest reply like this:

Channel::with('latestReply')->get()->sortByDesc('latestReply.created_at');

To get all channels from the user ordered by latest reply you would need that method:

public function getChannelsOrderdByLatestReply()
{
    return $this->channels()->with('latestReply')->get()->sortByDesc('latestReply.created_at');
}

where channels() is given by:

public function channels()
{
    return $this->belongsToMany('App\Channel');
}
Adam
  • 25,960
  • 22
  • 158
  • 247
1

Firstly, you don't have to specify the name of the pivot table if you follow Laravel's naming convention so your code looks a bit cleaner:

public function channels()
{
    return $this->belongsToMany('App\Channel') ...

Secondly, you'd have to call join explicitly to achieve the result in one query:

public function channels()
{
    return $this->belongsToMany(Channel::class) // a bit more clean
        ->withPivot('is_approved')
        ->leftJoin('replies', 'replies.channel_id', '=', 'channels.id') // channels.id
        ->groupBy('replies.channel_id')
        ->orderBy('replies.created_at', 'desc');
}
Hitori
  • 569
  • 1
  • 5
  • 21
Max Flex
  • 1,116
  • 10
  • 16
  • thanks, my code already looks like this. but it would not get all channels that has no replies yet – Hitori Nov 28 '16 at 08:22
  • @Hitori just use `leftJoin` then – Max Flex Nov 28 '16 at 08:25
  • @Hitori yes, `leftJoin` should do the trick. It would get all the channels even if there's no replies (take a look at my edit) – Max Flex Nov 28 '16 at 08:29
  • Thanks ! it works now. i also grouped replies by channel_id. – Hitori Nov 28 '16 at 08:31
  • Hi, @Quinn Daley. i noticed that the query i made seems like it takes only the first reply on the channel not on the latest one. – Hitori Nov 28 '16 at 10:21
  • @Hitori I don't quite understand what you mean. «Takes only the first reply on the channel not on the latest one»? – Max Flex Nov 28 '16 at 10:29
  • @Hitori. May be you should use 'desc' instead of 'asc' when ordering created_at timestamp. – Steve.NayLinAung Nov 28 '16 at 10:32
  • wait i'll post the result – Hitori Nov 28 '16 at 10:35
  • @QuinnDaley and steve. take a look at EDIT. – Hitori Nov 28 '16 at 10:40
  • @Hitori the reason lies somewhere else. Check how you sort data on the frontend. The query should return replies in the descending order – Max Flex Nov 28 '16 at 10:48
  • @Hitori, In my opinion, the query is correct and do its job. But this query is executed per user. That's why you also need to consider how you fetch your users from database. Ordering of user can also affect this list. Or may be due to some frontend ordering logic as Quinn said. – Steve.NayLinAung Nov 28 '16 at 10:54
  • i think it's not. i recently commented on public chat. now it should on the top list. but when i dumped the results of query. the public chat was still on the bottom of the result – Hitori Nov 28 '16 at 11:05
  • @Hitori i think i got it. It is because you group the query with channel id. – Steve.NayLinAung Nov 28 '16 at 11:16
  • @Steve.NayLinAung Hi, i grouped the query with channel id was because it would give the replies of each channels not the channels.. of course it will give the correct order of the replies. – Hitori Nov 28 '16 at 11:19
  • also, i tried to group by it replies.channel_id. same results. in the meantime, i used javascript Array.sorting to achieve what i want on the frontend – Hitori Nov 28 '16 at 11:22
1

If you have a hasOne() relationship, you can sort all the records by doing:

$results = Channel::with('reply')
                   ->join('replies', 'channels.replay_id', '=', 'replies.id')
                   ->orderBy('replies.created_at', 'desc')
                   ->paginate(10);

This sorts all the channels records by the newest replies (assuming you have only one reply per channel.) This is not your case, but someone may be looking for something like this (as I was.)

Gjaa
  • 1,461
  • 1
  • 19
  • 20