20

是否可以在仍然通过关系访问的同时使用单独的 ID 数组订购关系集合?

设置Checklist有许多ChecklistItems 并且相关项目的所需顺序作为属性存在Checklist::$item_order。它只是按用户所需顺序排列的数字 ID 数组。:

class Checklist extends Model {
    protected $casts = ['item_order' => 'array'];

    public function items() {
        return $this->hasMany(ChecklistItem::class);
    }
}
class ChecklistItem extends Model {
    public function list() {
        return $this->belongsTo(Checklist::class);
    }
}

我以正常方式访问这种关系:

$list = Checklist::find(1234);

foreach ($list->items as $item) {
    // More Code Here
}

是否有可行的方法根据数组$list->items中的值进行排序?$list->item_order

(我不能只在“项目”表中添加一个“订单”列,因为每个“列表”的订单都会发生变化。)

4

3 回答 3

33

你可以这样做:

$order = $list->item_order;
$list->items->sortBy(function($model) use ($order){
    return array_search($model->getKey(), $order);
}

您也可以向模型添加一个属性访问器,它执行相同的操作

public function getSortedItemsAttribute() 
{
    if ( ! is_null($this->item_order)) {
        $order = $this->item_order;

        $list = $this->items->sortBy(function($model) use ($order){
            return array_search($model->getKey(), $order);
        });
        return $list;
    }
    return $this->items;
}

用法:

foreach ($list->sortedItems as $item) {
    // More Code Here
}

如果您在多个地方需要这种功能,我建议您创建自己的 Collection 类:

class MyCollection extends Illuminate\Database\Eloquent\Collection {

    public function sortByIds(array $ids){
        return $this->sortBy(function($model) use ($ids){
            return array_search($model->getKey(), $ids);
        }
    }
}

然后,在模型中实际使用该类覆盖newCollection()。在这种情况下,它将在ChecklistItems类中:

public function newCollection(array $models = array())
{
    return new MyCollection($models);
}
于 2015-01-23T22:53:51.273 回答
4

您可以尝试建立一个关系,以您正在查找的顺序返回结果。您仍然应该能够预先加载关系,并按指定顺序获得结果。这一切都假设该item_order字段是一个逗号分隔的 id 字符串列表。

public function itemsOrdered()
{
    /**
     * Adds conditions to the original 'items' relationship. Due to the
     * join, we must specify to only select the fields for the related
     * table. Then, order by the results of the FIND_IN_SET function.
     */
    return $this->items()
        ->select('checklist_items.*')
        ->join('checklists', 'checklist_items.checklist_id', '=', 'checklists.id')
        ->orderByRaw('FIND_IN_SET(checklist_items.id, checklists.item_order)');
}

或者,如果您不想对表/字段进行硬编码:

public function itemsOrdered()
{
    $orderField = 'item_order';

    $relation = $this->items();
    $parentTable = $relation->getParent()->getTable();
    $related = $relation->getRelated();
    $relatedTable = $related->getTable();

    return $relation
        ->select($relatedTable.'.*')
        ->join($parentTable, $relation->getForeignKey(), '=', $relation->getQualifiedParentKeyName())
        ->orderByRaw('FIND_IN_SET('.$related->getQualifiedKeyName().', '.$parentTable.'.'.$orderField.')');
}

现在你可以:

$list = Checklist::with('items', 'itemsOrdered')->first();
var_export($list->item_order);
var_export($list->items->lists('id'));
var_export($list->itemsOrdered->lists('id'));

只是一个警告:这是相当实验性的代码。它看起来可以处理我可用的少量测试数据,但我还没有在生产环境中做过这样的事情。此外,这仅在 HasMany 关系上进行测试。如果你在 BelongsToMany 或类似的东西上尝试这个确切的代码,你会遇到问题。

于 2015-01-24T02:35:08.163 回答
0

我最近不得不做这样的事情,但增加了并非所有物品都有特定顺序的要求。没有指定顺序的其余项目必须按字母顺序排列。

因此,我在接受的答案中尝试了该策略,使用自定义回调对集合进行排序,该回调试图解释丢失的项目。我让它与多个调用一起工作,orderBy()但它很慢。

接下来我尝试了另一个答案,首先使用 MySQLFIND_IN_SET()函数排序,然后使用项目名称。我必须进行排序FIND_IN_SET(...) DESC以确保列出的项目位于列表的顶部,但这也导致订购的项目顺序相反。


所以我最终做了这样的事情,通过对数组中的每个项目进行相等性检查,然后通过项目名称在数据库中进行排序。与公认的答案不同,这是作为一种简单的关系方法完成的。这意味着我可以保留 Laravel 的神奇属性,并且无论何时我得到该项目列表,它总是可以正确排序,而无需调用特殊方法。而且由于它是在数据库级别完成的,因此它比在 PHP 中执行它要快。

class Checklist extends Model {
    protected $casts = ['item_order' => 'array'];

    public function items() {
        return $this->hasMany(ChecklistItem::class)
            ->when(count($this->item_order), function ($q) {
                $table = (new ChecklistItem)->getTable();
                $column = (new ChecklistItem)->getKeyName();
                foreach ($this->item_order as $id) {
                    $q->orderByRaw("`$table`.`$column`!=?", [$id]);
                }
                $q->orderBy('name');
            });
    }
}

现在这样做:

$list = Checklist::with('items')->find(123);
// assume $list->item_order === [34, 81, 28]

结果是这样的查询:

select * from `checklist_items`
where `checklist_items`.`checklist_id` = 123 and `checklist_items`.`checklist_id` is not null
order by `checklist_items`.`id`!=34, `checklist_items`.`id`!=81, `checklist_items`.`id`!=28, `name` asc;
于 2022-01-19T20:36:04.590 回答