Laravel Eloquent 按关联关系排序

19

我有3个模型

  • User(用户)
  • Channel(频道)
  • Reply(回复)

模型关系

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

假设我有2个频道

  • channel-1
  • channel-2

channel-2 拥有比 channel-1 更晚的回复

现在,我想按照用户所在频道的当前回复来排序用户的频道,就像一些聊天应用程序一样。我该如何像这样排序用户的频道?

  • channel-2
  • channel-1

我已经尝试了一些代码,但没有任何效果。

// 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

编辑 另外,我尝试了这个。是的,我得到了预期的结果,但如果没有回复,它将无法加载所有频道。

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

对于此情况,优先使用“join”而不是“with”。 - kapilpatwa93
嗯,所以我不能使用Eloquent来完成这个任务吗? - Hitori
@kapil.dev 嗨,我尝试了join方法,它按照我的预期工作。但它只会返回所有有回复的频道。 - Hitori
1
使用左连接,语法:join('表名','表名.col1','=','table2.fkcol','left') - kapilpatwa93
这个包可以帮助你 https://github.com/fico7489/laravel-eloquent-join - fico7489
4个回答

18

根据我的了解,eager load with 方法会运行第二个查询。这就是为什么你不能通过 eager loading with 方法实现你想要的结果。

我认为使用关联方法join方法相结合是解决办法。以下解决方案已经全面测试并且效果良好。

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

然后,您可以调用$user->sortedChannels('desc')来获取按回复created_at属性排序的频道列表

对于像频道这样可能有或可能没有回复的情况,只需使用leftJoin方法即可。

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

编辑:

如果您想向查询添加groupBy方法,则需要特别注意您的orderBy子句。因为在Sql中,Group By子句会在Order By子句之前运行。请参见此stackoverflow问题了解详细信息。

因此,如果您添加groupBy方法,必须使用orderByRaw方法,并且应该像下面这样实现。

return $this->channels()
                ->leftJoin('replies', 'channels.id', '=', 'replies.channel_id')
                ->groupBy(['channels.id'])
                ->orderByRaw('max(replies.created_at) desc')
                ->get();

为什么在已经使用 Eloquent 关联的情况下要使用 join?我建议只使用 join 并应用 orderby。 - kapilpatwa93
2
由于连接表的字段将在同一行上,因此可以进行排序。 - The Alpha
2
所以我使用了join方法,像这样。 return $this->belongsToMany('App\Channel', 'channel_user') ->withPivot('is_approved') ->join('replies', 'replies.channel_id', '=', 'channels.id') ->orderBy('replies.created_at', 'DESC'); 但是当特定的频道没有回复时,会导致错误。 - Hitori
@Hitori。我会在自己的帖子下面发表评论,因为我不想打扰其他人的帖子。我仔细分析了你的问题。首先,你最初的问题是关于查询频道的。但现在你描述的是关于查询回复的问题。请确切地描述问题。是“查询频道”还是“查询每个频道的回复”。 - Steve.NayLinAung
@Steve.NayLinAung,非常抱歉给你带来了误导。嗯,可以将其类比于Facebook消息。我想按其回复创建时间显示所有频道。 - Hitori
显示剩余2条评论

4

在您的频道类中,您需要创建此hasOne关系(您的频道hasMany回复,但它hasOne 最新回复):

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

您现在可以按最新回复的顺序获取所有频道,方法如下:

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

要按最新回复顺序获取用户的所有频道,您需要使用以下方法:

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

其中channels()由以下内容给出:

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

1
首先,如果你遵循 Laravel 命名规范,就不需要指定数据透视表的名称,这样你的代码看起来会更加简洁:
public function channels()
{
    return $this->belongsToMany('App\Channel') ...

其次,您需要显式调用join才能在一个查询中实现结果:
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
@Hitori 是的,leftJoin 应该可以解决问题。即使没有回复,它也会获取所有频道(请查看我的编辑)。 - Max Flex
谢谢!现在它可以正常工作了。我还将回复按channel_id分组。 - Hitori
等一下,我会发布结果。 - Hitori
@Hitori 我想我明白了。这是因为你将查询与频道ID分组了。 - Steve.NayLinAung
显示剩余10条评论

1
如果您有一个hasOne()关系,您可以通过以下方式对所有记录进行排序:
$results = Channel::with('reply')
                   ->join('replies', 'channels.replay_id', '=', 'replies.id')
                   ->orderBy('replies.created_at', 'desc')
                   ->paginate(10);

这将按照最新的回复对所有频道记录进行排序(假设每个频道只有一个回复)。虽然这不是您的情况,但可能有人正在寻找类似的内容(就像我一样)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接