Laravel - 按照关系对象的属性对集合数组进行排序

4
我是一个有用的助手,可以为您翻译文本。

我有两个表格,一个是带有字段product_codemagazines表格,另一个是issues表。它们具有belongsToMany关系。

杂志模型:

 public function issues()
 {
     return $this->hasMany('App\Issue');
 }

问题模型:
public function magazine()
{
    return $this->belongsTo('App\Magazine');
}

目前我有一个查询,它按杂志ID分组获取问题集合,并按最后一期的日期排序。

$issues = Issue::orderBy('date', 'desc')->get()->groupBy('magazine_id');

这是我的查询结果的样子:
Collection {#431 ▼
  #items: array:23 [▼
    103 => Collection {#206 ▼
      #items: array:52 [▶]
    }
    106 => Collection {#216 ▶}
    124 => Collection {#452 ▶}
    112 => Collection {#451 ▶}
    115 => Collection {#450 ▶}
    123 => Collection {#449 ▶}
    107 => Collection {#448 ▶}
    113 => Collection {#447 ▶}
    117 => Collection {#446 ▶}
    109 => Collection {#445 ▶}
    110 => Collection {#444 ▶}
    121 => Collection {#443 ▶}
    120 => Collection {#442 ▶}
    114 => Collection {#441 ▶}
    116 => Collection {#440 ▶}
    118 => Collection {#439 ▶}
    126 => Collection {#438 ▶}
    125 => Collection {#437 ▶}
    119 => Collection {#436 ▶}
    122 => Collection {#435 ▶}
    105 => Collection {#434 ▶}
    111 => Collection {#433 ▶}
    104 => Collection {#432 ▶}
  ]
}

所以,由于我有24本杂志,数组中有24个收藏夹,每个收藏夹包含一本杂志期刊。这些收藏夹按照每个收藏夹最新期刊的日期排序,每个收藏夹内部的期刊也按日期排序。因此,数组中的第一个收藏夹将是表格issues中最新期刊所在的收藏夹,第二个收藏夹将是同一表格中第二新的期刊所在的收藏夹,依此类推。
由于我将获得一个用户订阅的数组,其中包含像产品代码这样的内容:
$productCodes = ['aa1', 'bb2', 'cc3'];

我需要扩展这个查询并通过我将得到的$productCodes数组进一步对集合进行排序。我需要在表magazines中检查来自productCodes数组的代码,其中我有product_code字段。按magazine分组的期刊集合应该被排序,以便第一个集合是属于与数组productCodes中代码相同的magazine的集合,并且其中,最先的将是其集合具有最新问题的日期。然后,其余集合应按日期排序。我如何做出这样的查询?
更新
我已经尝试了@Paul Spiegel在答案中建议的代码,现在我得到了一个集合数组,其中包含杂志问题的集合。每个杂志集合中的问题均按日期排序,并且与$productCodes数组中的代码相同的杂志集合位于数组的开头,但杂志集合的数组仍未按每个杂志集合中最新问题的日期排序。

但问题在于product_code不是issues表中的字段,而是在关系表magazines中。 - Ludwig
啊,我明白了。我的错。那么:Issues::groupBy('magazine_id')->with('magazine' => function($q){ $q->orderBy('product_codes', 'desc')->get(); } )->orderBy('date', 'desc')->get();。我的 Laravel 技能有点生疏,但我认为这应该可以工作。 - Andrei
就像我说的,有点生疏了。忘记了在这种情况下with需要是一个数组。Issues::groupBy ('magazine_id')->with ([ 'magazine' => function ($q) { $q->orderBy ('product_codes', 'desc')->get (); } ])->orderBy ('date', 'desc')->get (); - Andrei
感谢您的努力,但我仍然遇到错误:SQLSTATE[42000]:语法错误或访问冲突:1055 SELECT列表中的表达式#1不在GROUP BY子句中,并且包含非聚合列'aftenposten.issues.id',它在GROUP BY子句中没有功能依赖性;这与sql_mode = only_full_group_by不兼容(SQL:select * from issuesgroup bymagazine_idorder bydate desc) - Ludwig
@RyanVincent 如果我表达不够清楚,我很抱歉。我想要的输出是现在的样子,一个包含24个集合数组的集合数组,就像问题中展示的那样。我想要添加的是,首先按照这个主要集合内部的24个集合进行排序,其中的项具有与数组productCodes中相同的产品代码,以及最新问题的日期。Paul Spiegel几乎满足我的需求,只是缺少对其中包含的最新问题日期进行排序的集合数组。 - Ludwig
显示剩余4条评论
3个回答

3

首先:不必使用Collection::groupBy(),直接使用关联即可:

$magazines = Magazine::with(['issues' => function($query) {
    $query->orderBy('date', 'desc');
}]);

这将为您提供一个杂志集合,包括相关问题的数组。 issues 数组按 date desc 排序。
现在,您可以使用两个带有不同条件的查询分割结果:
$baseQuery = Magazine::with(['issues' => function($query) {
    $query->orderBy('date', 'desc');
}]);

$query1 = clone $baseQuery;
$query2 = clone $baseQuery;

$query1->whereIn('product_code', $productCodes);
$query2->whereNotIn('product_code', $productCodes);

$query1将返回数组中所有带有product_code的杂志。 $query2将返回其他所有杂志。

现在您有两种方法可以组合这两个结果:

1)使用 Eloquent::unionAll()

$unionQuery = $query1->unionAll($query2);
$magazines = $unionQuery->get();

2)使用Collection::merge()方法

$magazines1 = $query1->get();
$magazines2 = $query2->get();
$magazines = $magazines1->merge($magazines2);

在这两种情况下,你将会得到相同的结果:杂志集合。具有product_code的杂志对象来自$productCodes数组,它们首先被排序。每个Magazine对象都包含一个issues数组,其中存放着相关的Issue对象。
如果你想要摆脱Magazine对象并且真正需要结果看起来像你的问题所描述的那样,你仍然可以这样做:
$issues = $magazines->keyBy('id')->pluck('issue');

但是可能并没有必要这样做。

更新

如果你真的需要将杂志集合的两个部分按最新的期号排序,我认为除了使用JOIN或在PHP中使用闭包进行排序之外,没有其他办法。使用JOIN对杂志进行排序还需要聚合。因此,我会切换回你原来的查询,并在上面展示的方式下添加一个JOIN将其分成两个部分:

$baseQuery = Issue::join('magazines', 'magazines.id', '=', 'issues.magazine_id');
$baseQuery->orderBy('issues.date', 'desc');
$baseQuery->select('issues.*');

$query1 = clone $baseQuery;
$query2 = clone $baseQuery;

$query1->whereIn('magazines.product_code', $productCodes);
$query2->whereNotIn('magazines.product_code', $productCodes);

然后

$issues1 = $query1->get()->groupBy('magazine_id');
$issues2 = $query2->get()->groupBy('magazine_id');

$issues = $issues1->union($issues2);

或者
$issues = $query1->unionAll($query2)->get()->groupBy('optionGroupId');

这种方法可以让具有相同产品代码的杂志排在数组前面,但是其他没有相同产品代码的杂志并没有按照最新期刊的日期排序。 - Ludwig
刚意识到,既有与数组产品代码相同的杂志,也有没有与产品代码相同的杂志,它们的最新问题日期都没有排序。 - Ludwig
1
我希望按最新问题排序不是必需的,只是偶然发生的 :-) - 我稍后会看一下。 - Paul Spiegel
尝试使用union()而不是merge()。 两个数组的键是不同的,因此不应有任何区别。 我不知道为什么merge()在这里不起作用。 - Paul Spiegel
这似乎是有效的,但我不确定问题对象上的图像属性为空,这是因为杂志和问题表中都有图像字段吗? - Ludwig
显示剩余2条评论

2

首先编写代码,最后进行逐步解释:

Magazine::all()
    ->load('issues')
    ->keyBy('id')
    ->sort(function ($a, $b) use ($product_codes) {
        return
            (in_array($b->product_code, $product_codes) - in_array($a->product_code, $product_codes))
            ?:
            (strtotime($b->latestIssueDate) - strtotime($a->latestIssueDate));
    })
    ->map(function ($m) {
        return $m->issues->sortByDesc('created_at');
    });

这将为您提供以下内容:
  • 所有杂志
  • 每个杂志的问题都会被急切加载
  • 在一个集合中,其中键是每个杂志的id
  • 按2个标准排序:首先是product_code是否在请求的代码中,然后是最新问题的日期(*)
  • 最后,对于每个杂志,您需要按日期降序排列其问题的集合。

如果我正确理解您的要求,这将为您提供所需结果。

(这假设日期列名为created_at。)


(*)为方便起见,我正在使用属性访问器:

public function getLatestIssueDateAttribute()
{
    // this breaks for Magazines with no Issues. Fix if necessary
    return $this->issues->sortByDesc('created_at')->first()->created_at;
}

@PaulSpiegel 不会,load 方法将在一个查询中获取所有相关模型。因此总共只有2个查询。 - alepeino
我的意思是 getLatestIssueDateAttribute() 方法。 - Paul Spiegel
1
我不确定。我觉得我的版本更易读。至于数据库优化,这种方法只需要2个查询(感谢Paul对我之前的次优版本的观察)。我也想听听@PaulSpiegel对你的问题的看法。 - alepeino
@PaulSpiegel 是的。我已经使用 DB::getQueryLog() 进行了测试。这个 $this->issues->sortBy... 操作是在之前已经预加载的问题集合上进行的,它不会再次查询数据库。 - alepeino
抱歉..我忽略了这个更改:->issues()-> => ->issues->。然而,通常在数据库中进行排序应该会更快。但对于小数据集可能没有什么影响。 - Paul Spiegel
显示剩余2条评论

1

首先,with需要是一个数组,在使用子查询时,不要调用get(),因为它会为每个Issue复制查询,从而产生一个很好的N+1问题。对于groupBy,只需将id作为第二个参数传递,以帮助解决only_full_group_by错误。

应该像这样:

$issues = Issue::groupBy('magazine_id', 'id')
    ->with('magazine')
    ->orderBy('date', 'desc')
    ->get();

这将按日期对您的Issue集合进行排序,然后在每个$issues->magazine中按product_code排序。如果您想通过date一次性排序,然后再按product_code排序,则需要执行联接操作,例如:
$issues = Issue::select('issues.*')
    ->groupBy('magazine_id', 'id')
    ->join('magazine', 'magazine.id', '=', 'issues.magazine_id')
    ->with('magazine')
    ->orderBy('issues.date', 'desc')
    ->orderBy('magazine.product_code', $productCodes)
    ->get();

更新

以下内容将按日期分组列出杂志集合,并按照$productCodes数组中指定的顺序按产品代码对每个杂志集合进行排序:

$issues = Issue::groupBy('magazine_id', 'id')
    ->with('magazine')
    ->orderBy('date', 'desc')
    ->get()
    ->groupBy('magazine_id')
    ->sortBy(function($issue) use ($productCodes) {
        $index = array_search($issue->first()->magazine->product_id, $productCodes);

        return $index !== false ? $index : $issue->magazine_date;
    });

我仍然在一个数组中获取所有问题,而不是问题集合的数组。我也发现我没有清楚地表达我的查询结果。我已经在我的问题中进行了更正。 - Ludwig
我已经尝试进一步解释我在这里想要实现的内容。 - Ludwig
很遗憾,它不起作用,我得到了一个未定义变量$issue的错误。 - Ludwig
糟糕,编辑了。$issue 应该被注入到 sortBy 回调函数中。 - Eric Tucker
然后我得到了 Undefined property: Illuminate\Database\Eloquent\Collection::$magazine - Ludwig
显示剩余6条评论

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