如何合并两个Eloquent集合?

136

我有一个问题表和一个标签表。我想从给定问题的标签中获取所有问题。例如,给定问题可能附有标签“旅行”,“火车”和“文化”。我想能够获取这三个标签的所有问题。看起来棘手的问题是,问题和标签之间的关系是Many-To-Many,在Eloquent中定义为belongsToMany。

我考虑尝试合并以下问题集合:

foreach ($question->tags as $tag) {
    if (!isset($related)) {
        $related = $tag->questions;
    } else {
        $related->merge($tag->questions);
    }
}

虽然看起来它不起作用。似乎没有合并任何东西。我这样尝试是正确的吗?此外,在Eloquent中获取多对多关系中的一行行数据也许有更好的方法吗?


你有查看关于急切加载和with方法的文档吗?使用更好的Eloquent查询可以轻松解决你的问题。等我上电脑后,如果没有其他人先做,我会写一个示例。 - Luceos
1
@Luceos with 不起作用。需要使用 whereHas - 就像下面的答案中所示。 - Jarek Tkaczyk
是的,我的错误;你是正确的。 - Luceos
9个回答

202

merge方法返回合并后的集合,不会改变原始集合,因此您需要按照以下步骤进行操作

$original = new Collection(['foo']);

$latest = new Collection(['bar']);

$merged = $original->merge($latest); // Contains foo and bar.

将这个示例应用到您的代码中

$related = new Collection();

foreach ($question->tags as $tag)
{
    $related = $related->merge($tag->questions);
}

1
我试图从一棵树中构建一个平面列表,使用push是我所需要的,但foreach方法确实很有帮助。 - George
1
请注意,Eloquent集合的行为与普通集合不同,即它们使用getKey来合并结果,因此Model :: all()->merge(Model :: all())->count() === Model :: all()->count() - eithed
请注意,如果提供的 $items 中存在与原始第二个集合具有相同值的字符串键,则 merge 方法将替换原始第一个集合中的任何项。如果第一个集合的键是数字,则第二个集合将添加到新集合的末尾。https://laravel.com/docs/8.x/collections#method-merge - shintaroid
我认为需要去重。 - mercury

48

Collection 上的 merge() 方法不会修改它被调用的集合。它返回一个新的集合,其中包含合并的新数据。您需要:

$related = $related->merge($tag->questions);

然而,我认为你从错误的角度着手解决问题。

由于您正在寻找符合某些标准的问题,因此以这种方式查询可能会更容易。使用has()whereHas()方法基于相关记录的存在生成查询。

如果您只想查找具有任何标签的问题,则可以使用has()方法。由于您要查找具有特定标签的问题,因此应使用whereHas()添加条件。

因此,如果您想要具有至少一个标签('Travel'、'Trains'或'Culture')的所有问题,请使用以下查询:

$questions = Question::whereHas('tags', function($q) {
    $q->whereIn('name', ['Travel', 'Trains', 'Culture']);
})->get();

如果您想要包含这三个标签的所有问题,您的查询应该如下所示:

$questions = Question::whereHas('tags', function($q) {
    $q->where('name', 'Travel');
})->whereHas('tags', function($q) {
    $q->where('name', 'Trains');
})->whereHas('tags', function($q) {
    $q->where('name', 'Culture');
})->get();

1
+,然而你提出的第二个选项(所有标签)可能可以简化:http://stackoverflow.com/a/24706347/784588 - Jarek Tkaczyk
但您不能硬编码标签名称。在此示例中,问题恰好具有这些标签,但在其他问题中,标签将会有所不同。 - Allfarid Morales García

35
$users = User::all();
$associates = Associate::all();

$userAndAssociate = $users->merge($associates);

10
请阅读(覆盖)此链接:https://medium.com/@tadaspaplauskas/quick-tip-laravel-eloquent-collections-merge-gotcha-moment-e2a56fc95889此文介绍了有关使用Laravel Eloquent集合的一些注意事项,特别是当使用merge方法时需要注意的问题。作者提供了代码示例和详细说明,让读者更好地理解如何避免可能出现的错误。值得注意的是,在merge方法中传递一个纯数组将不起作用,因为它们不能与Eloquent集合合并。最后,文章总结了这个"gotcha moment"的解决方案,以及在遇到类似问题时应该采取的措施。 - Jeffz
4
@Jeffz,仅基于ID合并“重复项”真的令人难以置信。 - andrewtweber
合并(merge)是用于合并的,连接(concat)是用于连接的。 - jaumebalust

20

将两个不同的Eloquent集合合并成一个,如果某些对象恰好具有相同的ID,则其中一个将覆盖另一个。使用push()方法或重新考虑解决问题的方法来避免这种情况。

参见网页

2
谢谢,这让我找到了这里的评论 https://medium.com/@jeffparr_57441/what-you-describe-happens-when-initial-eloquent-collection-in-your-case-cars-has-same-id-as-701e5a217372 ,看起来可以干净地完成工作而不会覆盖。 - Mark
使用 push() 更安全。 - shintaroid

16

对于每个Eloquent集合创建一个新的基础集合,这样合并对我有用。

$foo = collect(Foo::all());
$bar = collect(Bar::all());
$merged = $foo->merge($bar);
在这种情况下,主键不会产生冲突。

7

我在使用合并(merge)时遇到了一些问题,因此我使用了连接(concat)。您可以按以下方式使用它。

$users = User::all();
$associates = Associate::all();
$userAndAssociate = $users->concat($associates);

2

eloquent collections上,所有操作对我都无效。laravel eloquent collections使用来自项目的键我认为会导致合并问题,您需要将第一个集合作为数组返回,将其放入新集合中,然后将其他集合推入新集合中。

public function getFixturesAttribute()
{
    $fixtures = collect( $this->homeFixtures->all() );
    $this->awayFixtures->each( function( $fixture ) use ( $fixtures ) {
        $fixtures->push( $fixture );
    });
    return $fixtures;
}

0
我想补充一下,我发现concat方法似乎不会根据ID进行覆盖,而merge方法会。concat对我有用,而merge会导致问题。

-1

很抱歉,但自从PHP 7.4以来,您可以像这样操作(最好使用merge)。

$foo = Foo::all();
$bar = Bar::all();

/** $foo will contain $foo + $bar */
$foo->push(...$bar);

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