如何在Laravel中同步同一属性的多个值?

8

我正在使用Laravel 5.4开发一个具有多个产品属性功能的电子商务网站。一切都很顺利。但是,当我尝试在Pivot表中同步相同属性的多个值时,Laravel会忽略重复的值对。例如,我有一个名为“Network”的属性,它有3个值:2G、3G和4G。一部手机支持3G和4G网络。我想将3G和4G值同步到数据库中。然而,Laravel忽略了其中一个。

Products Table: 

ID - Product Name
1  - Test Mobile



Attributes Table

ID - AttributeName
1 - Network



AttributeValues Table

ID - AttributeID - AttributeValue
1  - 1           - 2G
2  - 1           - 3G
3  - 1           - 4G



ProductAttributes Table

ID - AttributeID - ProductID - AttributeValue
1  - 1           - 1         - 3G
1  - 1           - 1         - 4G

我希望将产品属性存储在“ProductAttributes”表中,就像这样。但是Laravel忽略了其中一个。

我是这样保存数据的:

    $product = Product::create([
        'name' => 'Test Mobile'
    ]);

    $product->attributes()->sync([
        1 => ['AttributeValue' => '3G'], 
        1 => ['AttributeValue' => '4G']
    ]);

有什么建议或想法吗?

您的数组存在重复的键。 - user1897253
在ProductAttributes表中,您应该使用AttributeValues.ID字段代替AttributeID。这样关联就会变得唯一。 - Shadow
@Shadow 很好的观点。您能告诉我如何通过产品模型访问属性表吗? - Rizwan Khan
5个回答

20

我知道现在已经晚了两年,但是今天我也遇到了同样的问题,并想留下解决方案,以防将来有人寻找。如果您使用的是原始代码:

    $product->attributes()->sync([
        1 => ['AttributeValue' => '3G'], 
        1 => ['AttributeValue' => '4G']
    ]);

数组中的第二个项目将覆盖第一个项目,因此最终数据库中只会有一个“4G”条目。这实际上不是laravel问题,而是PHP关联数组的实现方式——基本上不能在同一索引上拥有两个项目。

实际上有两种解决方法:

1)第一种方法效率非常低,但它是可行的。我把它留在这里只是为了记录,因为那是我最初解决问题的方式。您需要使用类似于以下内容而不是您的代码:

    $product->attributes()->sync([]);  // empty relation completely

    // add first item
    $product->attributes()->syncWithoutDetaching([
        1 => ['AttributeValue' => '3G'],
    ]);

    // add second item without detaching the first one
    $product->attributes()->syncWithoutDetaching([
        1 => ['AttributeValue' => '4G'], 
    ]);

这种方法极其低效,因为它需要一次查询来从关系中删除所有现有数据,然后逐个添加新项目。你也可以在循环内运行syncWithoutDetaching,总的低效率将大大取决于您需要同步多少个项目。

2) 第一种解决方案不够好,所以我继续深入挖掘和尝试,并找到了一种方法来实现这个目标。与其将您的项目放在数组的特定索引上,不如发送没有给定特定索引的数组,并将ID放在数组本身中。像这样的东西

    $product->attributes()->sync([
        ['AttributeID' => 1, 'AttributeValue' => '3G'], 
        ['AttributeID' => 1, 'AttributeValue' => '4G']
    ]);

通过这种方式,您实际上可以将具有相同AttributeID的两个项目发送到sync()方法,而不会一个覆盖另一个。


2
但是在laravel 5.7中仍然存在问题。我会继续深入挖掘并更新。 - graphicmist
在 Laravel 8.5 中,syncWithoutDetaching() 似乎无法正常工作,我使用了 sync([]) 和 attach()。 - mehdi jalilvand
你能提供一下使用sync方法的文档链接吗?我在任何地方都找不到关于使用AttributeID的参考。谢谢。 - Nite
太棒了!我不知道Laravel在sync方法中支持这种数组方案。 - undefined

4

目前,

$product->attributes()->sync([]);

$product->attributes()->sync([
    ['AttributeID' => 1, 'AttributeValue' => '3G'], 
    ['AttributeID' => 1, 'AttributeValue' => '4G']
]);

1

看了Becquerel在4月5日的回复,回复#2,并阅读了/laravel/framework/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.phpsync()方法的源代码(我认为这是Laravel 2.4),我没有看到(或者说,无法识别)支持这种“数组的数组”功能的代码。事实上,这是整个方法的源代码:

public function sync($ids, $detaching = true)
{
    $changes = [
        'attached' => [], 'detached' => [], 'updated' => [],
    ];

    if ($ids instanceof Collection) {
        $ids = $ids->modelKeys();
    }

    // First we need to attach any of the associated models that are not currently
    // in this joining table. We'll spin through the given IDs, checking to see
    // if they exist in the array of current ones, and if not we will insert.
    $current = $this->newPivotQuery()->pluck($this->otherKey);

    $records = $this->formatSyncList($ids);

    $detach = array_diff($current, array_keys($records));

    // Next, we will take the differences of the currents and given IDs and detach
    // all of the entities that exist in the "current" array but are not in the
    // the array of the IDs given to the method which will complete the sync.
    if ($detaching && count($detach) > 0) {
        $this->detach($detach);

        $changes['detached'] = (array) array_map(function ($v) {
            return is_numeric($v) ? (int) $v : (string) $v;
        }, $detach);
    }

    // Now we are finally ready to attach the new records. Note that we'll disable
    // touching until after the entire operation is complete so we don't fire a
    // ton of touch operations until we are totally done syncing the records.
    $changes = array_merge(
        $changes, $this->attachNew($records, $current, false)
    );

    if (count($changes['attached']) || count($changes['updated'])) {
        $this->touchIfTouching();
    }

    return $changes;
}

现在,Laravel 充满了依赖注入和其他魔法(显然类似于 Perl 的“map”概念),但我没有看到任何东西能够做到 Becquerel 所说的那样。总的来说,Laravel 的文档并没有明确说明它在多对多关系中重复值会发生什么,或者它是否有意识地处理它们。
我还注意到,上面显示的方法的实现实际上与他引用的“替代方案1”非常相似,后者被称为“极其低效”。它似乎将键分类为三个桶...按照我的理解从不允许重复...然后根据需要执行插入、更新和删除操作。(我没看到任何 SQL“事务”,这也让我非常惊讶...它们是以某种神奇的方式存在吗?)
当外部表中的相关记录集中出现超过一个值时,我根本无法确定 Laravel 是否像返回数组一样返回它们。
我写了这篇冗长的回答,希望引起更多的评论。谢谢。

“Becquerel,我相信你是对的。” 当然。但该死的是,我看不出在代码中为什么你是正确的!:=) Laravel所谓的文档只是一场对话,并没有给我答案。源代码也没有。 - Mike Robinson
FYI...也许这部分是因为我被迫使用了一个相当旧的Laravel版本,但我最终成功的做法是使用DB::transaction()来包含一个短例程,首先执行detach()以删除所有现有的子条目,然后循环遍历数组以attach()替换。简单明了,它完美地工作了。*(事务确保是“一切或者什么都没有”。要么所有SQL语句同时生效,要么就一个也不生效。)* - Mike Robinson
你有没有想明白为什么 Becquerel 的方法有效(如果它确实有效)?最近我也遇到了这个问题,看了 Laravel 8 上的 sync() 实现,也无法理解为什么 "AttributeID" 可以工作。最终我采用了你提到的使用事务的方法。 - Nite

-1
使用同步方法来使用关系在控制器中存储/更新数据:
$product-> attribute()->sync($request->input('product_ids', []));

-3

Laravel 中的 sync() 函数会自动获取重复项的读取。您可以使用它来强制执行。

$product->attribute()->syncWithoutDetaching([
    1 => ['AttributeValue' => '3G'], 
    1 => ['AttributeValue' => '4G']
]);

祝你好运,伙计!


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