Reputation: 484
I developing an eCommerce ( with Multiple Product Attributes feature ) website using Laravel 5.4. Everything is working fine. But When I try to sync multiple values of the same Attribute in Pivot table. Laravel ignores the duplicate pares. For example, I've an Attribute called "Network" which has 3 values: 2G, 3G, 4G. A mobile supports 3G and 4G network. I want to sync 3G and 4G value in database. Laravel ignores one of them.
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
I want to store the Product Attributes in "ProductAttributes" table something like that. But Laravel Ignore one of them.
I am saving the data like that:
$product = Product::create([
'name' => 'Test Mobile'
]);
$product->attributes()->sync([
1 => ['AttributeValue' => '3G'],
1 => ['AttributeValue' => '4G']
]);
Any suggestions, Ideas?
Upvotes: 9
Views: 8881
Reputation: 1366
For now,
$product->attributes()->sync([]);
$product->attributes()->sync([
['AttributeID' => 1, 'AttributeValue' => '3G'],
['AttributeID' => 1, 'AttributeValue' => '4G']
]);
Upvotes: 5
Reputation: 8995
Looking at Becquerel's response of April 5th, response #2, and in reading the source code of the sync()
method in /laravel/framework/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php
(this is, I think, Laravel 2.4), I do not see (or, cannot identify) code that would support this "array of array" functionality. In fact, here's the source-code of the entire method:
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;
}
Now, Laravel is full of dependency-injection and other Magick (apparently similar to Perl's notion of "map?"), but I don't see anything here that will do what Becquerel says it will. And, generally speaking, Laravel's documentation really doesn't come out and say what it does do with repeated values in many-to-many relationships, or if it is cognizant of them at all.
I also notice that the implementation of the method as shown above is actually very similar to the "alternative #1" that he cites as "extremely inefficient." It seems to classify the keys into three buckets ... never seeming to allow for repetition, by my reading ... and then to perform insert, update and delete operations as needed. (No SQL "transactions" anywhere, that I can see, which also surprises me very much ... are they "magickally" there somehow?)
I simply can't determine if Laravel, when presented with more than one occurrence of a value in the (set of related records in the) foreign table, does anything sensible like return them as an array.
I've made this very long-winded response in hopes of eliciting further comments. Thanks.
Upvotes: 1
Reputation: 406
I know this is two years late, but I was dealing with the same issue today, and figured I may leave the solution here, in case anyone looks for it in the future. If you use your original code:
$product->attributes()->sync([
1 => ['AttributeValue' => '3G'],
1 => ['AttributeValue' => '4G']
]);
the second item in the array will overwrite the first one, so in the end, there will only be a "4G" entry in the database. This is not really a laravel issue, it is how PHP associative arrays are implemented - you basically cannot have two items in the array on the same index.
There are actually two ways to solve this issue
1) first one is very inefficient, but it is functional. I am leaving it here only for the record, because that was the original way I solved the issue. Instead of your code, you would need something like this
$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'],
]);
this is EXTREMELY inefficient, because it needs one query to delete all existing data from the relation, and then add new items one by one. You could also run the syncWithoutDetaching
inside a loop, and overall inefficiency would greatly depend on how many items you need to sync.
2) the first solution was not good enough, so I kept digging and experimenting, and figured out a way how to make this happen. Instead of putting your items on specific index in the array, you can send array without specific indexes given, and put the ID in the array itself. Something like this
$product->attributes()->sync([
['AttributeID' => 1, 'AttributeValue' => '3G'],
['AttributeID' => 1, 'AttributeValue' => '4G']
]);
by doing it this way, you can actually send two items with the same AttributeID
to the sync() method, without one overwriting the other one
Upvotes: 21
Reputation: 21
Use the sync method to store/update the data in the Controller using the relationship :
$product-> attribute()->sync($request->input('product_ids', []));
Upvotes: -1
Reputation: 13259
sync()
function in Laravel automatically get reads of duplicates. You can force it with
$product->attribute()->syncWithoutDetaching([
1 => ['AttributeValue' => '3G'],
1 => ['AttributeValue' => '4G']
]);
Good luck mate!
Upvotes: -4