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?
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
For now,
$product->attributes()->sync([]);
$product->attributes()->sync([
['AttributeID' => 1, 'AttributeValue' => '3G'],
['AttributeID' => 1, 'AttributeValue' => '4G']
]);
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.
Use the sync method to store/update the data in the Controller using the relationship :
$product-> attribute()->sync($request->input('product_ids', []));
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!
Related
I am attaching user_id, product_id with an extra field. Every thing is working fine until the extra field should be updated. When the field will be filled for second time instead of updating it will add another one to database. and it's obvious because I used attach instead of sync. But when I use sync I get an error.
this is my code:
$price = $request->input('price');
$product = Product::find($id);
$product->users()->attach(Auth::id(), ['price' => $price]);
and this is the error I get when I use sync:
Argument 1 passed to
Illuminate\Database\Eloquent\Relations\BelongsToMany::formatRecordsList()
must be of the type array, integer given
The first parameter of the sync() method should be an array. So correct syntax is:
$product->users()->sync([Auth::id() => ['price' => $price]]);
https://laravel.com/docs/5.4/eloquent-relationships#updating-many-to-many-relationships
Sync method accepts an array of IDs to place on the pivot table
also the sync method will delete the models from table if model does not exist in array and insert new items to the pivot table.
So you need to do
$product->users()->sync([Auth::id() => ['price' => $price]]);
The sync method accepts an array of IDs to place on the intermediate table.Any IDs that are not in the given array will be removed from the intermediate table.So you should pass an array as a first parameter to sync() function as
$product->users()->sync([Auth::id() => ['price' => $price]]);
I assume that this should all be in one query in order to prevent duplicate data in the database. Is this correct?
How do I simplify this code into one Eloquent query?
$user = User::where( 'id', '=', $otherID )->first();
if( $user != null )
{
if( $user->requestReceived() )
accept_friend( $otherID );
else if( !$user->requestSent() )
{
$friend = new Friend;
$friend->user_1= $myID;
$friend->user_2 = $otherID;
$friend->accepted = 0;
$friend->save();
}
}
I assume that this should all be in one query in order to prevent
duplicate data in the database. Is this correct?
It's not correct. You prevent duplication by placing unique constraints on database level.
There's literally nothing you can do in php or any other language for that matter, that will prevent duplicates, if you don't have unique keys on your table(s). That's a simple fact, and if anyone tells you anything different - that person is blatantly wrong. I can explain why, but the explanation would be a lengthy one so I'll skip it.
Your code should be quite simple - just insert the data. Since it's not exactly clear how uniqueness is handled (it appears to be user_2, accepted, but there's an edge case), without a bit more data form you - it's not possible to suggest a complete solution.
You can always disregard what I wrote and try to go with suggested solutions, but they will fail miserably and you'll end up with duplicates.
I would say if there is a relationship between User and Friend you can simply employ Laravel's model relationship, such as:
$status = User::find($id)->friends()->updateOrCreate(['user_id' => $id], $attributes_to_update));
Thats what I would do to ensure that the new data is updated or a new one is created.
PS: I have used updateOrCreate() on Laravel 5.2.* only. And also it would be nice to actually do some check on user existence before updating else some errors might be thrown for null.
UPDATE
I'm not sure what to do. Could you explain a bit more what I should do? What about $attributes_to_update ?
Okay. Depending on what fields in the friends table marks the two friends, now using your example user_1 and user_2. By the example I gave, the $attributes_to_update would be (assuming otherID is the new friend's id):
$attributes_to_update = ['user_2' => otherID, 'accepted' => 0 ];
If your relationship between User and Friend is set properly, then the user_1 would already included in the insertion.
Furthermore,on this updateOrCreate function:
updateOrCreate($attributes_to_check, $attributes_to_update);
$attributes_to_check would mean those fields you want to check if they already exists before you create/update new one so if I want to ensure, the check is made when accepted is 0 then I can pass both say `['user_1' => 1, 'accepted' => 0]
Hope this is clearer now.
I'm assuming "friends" here represents a many-to-many relation between users. Apparently friend requests from one user (myID) to another (otherId).
You can represent that with Eloquent as:
class User extends Model
{
//...
public function friends()
{
return $this->belongsToMany(User::class, 'friends', 'myId', 'otherId')->withPivot('accepted');
}
}
That is, no need for Friend model.
Then, I think this is equivalent to what you want to accomplish (if not, please update with clarification):
$me = User::find($myId);
$me->friends()->syncWithoutDetaching([$otherId => ['accepted' => 0]]);
(accepted 0 or 1, according to your business logic).
This sync method prevents duplicate inserts, and updates or creates any row for the given pair of "myId - otherId". You can set any number of additional fields in the pivot table with this method.
However, I agree with #Mjh about setting unique constraints at database level as well.
For this kind of issue, First of all, you have to enjoy the code and database if you are working in laravel. For this first you create realtionship between both table friend and user in database as well as in Models . Also you have to use unique in database .
$data= array('accepted' => 0);
User::find($otherID)->friends()->updateOrCreate(['user_id', $otherID], $data));
This is query you can work with this . Also you can pass multiple condition here. Thanks
You can use firstOrCreate/ firstOrNew methods (https://laravel.com/docs/5.3/eloquent)
Example (from docs) :
// Retrieve the flight by the attributes, or create it if it doesn't exist...
$flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);
// Retrieve the flight by the attributes, or instantiate a new instance...
$flight = App\Flight::firstOrNew(['name' => 'Flight 10']);
use `firstOrCreate' it will do same as you did manually.
Definition of FirstOrCreate copied from the Laravel Manual.
The firstOrCreate method will attempt to locate a database record using the given column / value pairs. If the model can not be found in the database, a record will be inserted with the given attributes.
So according to that you should try :
$user = User::where( 'id', '=', $otherID )->first();
$friend=Friend::firstOrCreate(['user_id' => $myId], ['user_2' => $otherId]);
It will check with both IDs if not exists then create record in friends table.
Can somebody explained me why I need to pass two closed array [[]] in my sync parameter? I tried to use one array with multiple model inside and it's not syncing. I tried two array and it works. Any tips would appreciated!
$document = new Document();
foreach($request->recipient as $recipientId)
{
$document->notifications()->sync([['user_id' => $recipientId, 'sender_id' => $user->id, 'notification_id' => 4]],false);
}
Simply the sync method expects an array of id for the models to attach, and any other pivot changes in the second array.
Please check below the example from the documentation:
$user->roles()->sync([1 => ['expires' => true], 2, 3]);
You can find out more on what the sync method expects in the documentation, https://laravel.com/docs/5.2/eloquent-relationships#inserting-many-to-many-relationships.
At its most basic level, the sync method accepts an array of ids.
You can also sync an array of arrays where each child array contains intermediate table values, which is what you are doing. You are only syncing one item though, thus the double brackets.
As a novice to laravel I am hoping I can get to the bottom of this rather irritating issue!
I have a small app that consists of 2 user types, Buyers and Providers, The usertype->buyers can request a free estimation for a service they require and the provider can create this manually or have an estimation sent automatically based on what they set as there default value for that specific price range so for example if a user(buyer) requests an estimation for a service that is £200 the provider may have set a default estimation of £180 - £220 for that price range and this is what will be shown to the user.
The problem I have is to make it fair we use the random function within laravel to randomly select 5 providers from the providers_table along with their default estimation and show this to the user from orderBy in descending order and looping through the results in the view.
$projectbids = ProjectBids::where('provider_id', '=', $proid)
->where('category_id', '=', $catid)
->orderBy('bid_price', 'desc')
->get()->random(5);
foreach($projectbids as $bid)
{{ $bid->bid_price }}
endforeach
So I'm trying to find a way to insert the 5 displayed estimations into a saved_estimates table so if the user refreshes the page or comes back to the page the model and view will not reorder the providers results based from the random(5) function that gets called.
I'm aware of this question: Create a Insert... Select statement in Laravel but wasn't to sure if there could be a better or easier way of doing this as my problem lays where I'm looping through the 5 results provided to the view from the table.
In a nutshell: I want to be able to store the 5 dynamic results straight into a saved_estimations table with several columns such as project_id, provider_id, user_id, estimated_price so the user can come back to their saved estimations in the future without them changing.
Very grateful for any input and advice, it will be very helpful to see what the more experienced users would suggest.
Thanks
-- Updated personal approach to see if there is room for improvement and to make it more laravel friendly.
$results = $bidders;
foreach($results as $row){
$project = $projectid;
$category = $row->category_id;
$provider = $row->service_providers_id;
$bidprice = $row->bid_price;
$propvalue = $row->property_value_id;
DB::table('static_bids')->insert(array(
'project_id' => $project,
'category_id' => $category,
'service_providers_id' => $provider,
'bid_price' => $bidprice,
'property_value_id' => $propvalue
));
}
All you need is to use simple insertions into the table, something like this:
foreach ($projectbids as $bid) {
$result = SavedEstimation::create(array(
'project_id' => $bid->project_id,
'provider_id'=> $bid->provider_id,
'user_id' => $bid->user_id,
'estimated_price' => $bid->estimated_price
));
}
There is no way to insert this by using just one query anyway. You could simply store output HTML if the only reason you want to do this is to show same HTML to the user later, but this is bad practice.
I have a Symfony Standard Distribution app and use Doctrine's query builder and result cache to speed up database queries. I also assign unique ids to all my caches such as
....
->getQuery()
->useResultCache(true, 2592000, 'single_product_query_id_'.$id)
->getOneOrNullResult();
....
When a product field changes I can delete this specific cache using
....
$em = $this->getDoctrine()->getManager();
$cacheDriver = $em->getConfiguration()->getResultCacheImpl();
$cache = $cacheDriver->fetch('single_product_query_id_'.$id);
if($cache){
$cacheDriver->delete('single_product_query_id_'.$id);
}
....
Deleting the cache obviously make the changes visible instantly but I found this method to be a waste because in my controller I already have the up-to-date data to be persisted in my db using doctrine so I resolved to updating my cache instead of deleting it. This is where my challenge lies because the documentation is not clear on how to do this apart from the code to save the cache which is
....
$cacheDriver->save('single_product_query_id_'.$id, $new_cache, $new_lifetime);
....
So I dug deeper by passing my cache as variable $cache to a twig template and viewing via var dump. It looked like this
....
array (size=2)
'SELECT d0_.id AS id_0, d0_.title AS title_1, d0_.imagepath AS imagepath_2, d0_.description AS description_3,.......................
array (size=1)
0 =>
array (size=57)
'id_0' => string '1' (length=1)
'title_1' => string ''Tasty Thursday'!' (length=17)
'imagepath_2' => string 'main.jpeg' (length=9)
'description_3' => string 'this and every Thursday, buy a 2kg forest cake and get a 1kg fruity forest cake FREE!!!' (length=87)
...........................
I do not know how doctrine generates this multidimensional array, I would like to get this function(s) so that I can properly populate my cache to update.
Currently, I successfully update my cache by var dumping my cache to find out which row I need to update then iterating through the array. But every time I get a new idea and add a new row to an entity I have to repeat this process especially if this entity was joined to product table.
So my question is, how can I manually build a doctrine cache array from query result or better entity?