MySQL Transaction doesn't rollback with 0 affected rows - Laravel - php

I tried to make my first query return affected rows: 0 to see if the transaction fails but it continued executing the second query.
Should i break the transaction manually?
DB::transaction(function () {
User::where('id', 1002)->update(['name' => 'x']); // id:1002 doesn't exist
Post::where('user_id', 1)->update(['title' => 'New Title']);
});

There's not a lot of context around your sample code, but a very basic approach would be something like this:
$user = User::findorFail(1002);
$user->update(['name' => 'x']);
if ($user->wasChanged('name')) {
Post::where('user_id', 1)->update(['title' => 'New Title']);
}
So the first line will throw an exception if the model isn't found. Then we do an update. You specifically said you were checking for 0 affected rows, so next we use the wasChanged() method. It "determines if any attributes were changed when the model was last saved within the current request cycle." If that's the case, we proceed with the next update.
There are other changes that could be made involving, for example, route model binding if there was more of your code shown in the question, but hopefully this is a helpful start.

Related

How to lock database for Laravel's `firstOrCreate`?

We currently encounter a Duplicate entry QueryException when executing the following code:
Slug::firstOrCreate([
Slug::ENTITY_TYPE => $this->getEntityType(),
Slug::SLUG => $slug
], [
Slug::ENTITY_ID => $this->getKey()
]);
Since the firstOrCreate method by Laravel first checks if the entry with the attributes exist before inserting it, this exception should never occur. However, we have an application with million of visitors and million of actions every day and therefore also use a master DB connection with two slaves for reading. Therefore, it might be possible that some race conditions might occur.
We currently tried to separate the query and force the master connection for reading:
$slugModel = Slug::onWriteConnection()->where([
Slug::SLUG => $slug,
Slug::ENTITY_TYPE => $this->getEntityType()
])->first();
if ($slugModel && $slugModel->entity_id !== $this->getKey()) {
$class = get_class($this);
throw new \RuntimeException("The slug [{$slug}] already exists for a model of type [{$class}].");
}
if (!$slugModel) {
return $this->slugs()->create([
Slug::SLUG => $slug,
Slug::ENTITY_TYPE => $this->getEntityType()
]);
}
However the exception still occurs sometimes.
Our next approach would be to lock the table before the reading check and release the lock after the writing to prevent any inserts with the same slug from other database actions between our reading and our writing. Does anyone know how to solve this? I don`t really understand how Laravel's Pessimistic Locking can help solving the issue. We use MySql for our database.
I would not recommend to lock the table, especially if you have millions of viewers.
Most race-conditions can be fixed by locks, but this is not fixable with locks, because you cannot lock a row that does not exist (there is something like gap locking, but this won't help here.).
Laravel does not handle race-conditions by itself. If you call firstOrCreate it does two queries:
SELECT item where slug=X and entity_type=Y
If it does not exists, create it
Now because we have two queries, race condition is possible, meaning two user in parallel reach step 1, then both try to create the entry in step 2 and your system will crash.
Since you already have a Duplicate Key error, it means you aleady put a unique constrain on the tuple on the two columns that identify your row, which is good.
What you could do now, is to catch the duplicate key error like this:
try{
$slug = Slug::firstOrCreate([
Slug::ENTITY_TYPE => $this->getEntityType(),
Slug::SLUG => $slug
], [
Slug::ENTITY_ID => $this->getKey()
]);
}
catch (Illuminate\Database\QueryException $e){
$errorCode = $e->errorInfo[1];
if($errorCode == 1062){
$slug = Slug::where('slug','=', $slug)->where('entity_type','=', $this->getEntityType())->first();
}
}
one solution for this is to use Laravel queue and make sure that it runs one job at a time, in this way you will never have 2 identical queries at the same time.
for sure this will not work if you want to return back the result in the same request.

Filter returning wrong results

... Either that or I'm not using / understanding it correctly. Take a look at my test snippet:
$subscriptions = auth()->user()->subscriptions()->get();
$id = '40';
$test = $subscriptions->filter(function ($subscription) use ($id) {
return $subscription->where('description_id', $id);
});
dd($test);
Currently there is one Subscription in the database, with a description_id of 20. When running the above test snippet, I still get the one result returned that has a description_id of 20. So, this is not an expected result. What am I doing wrong?
where() is used to filter a collection, but you're giving it a single subscription at a time when you use filter(). And since where() is also a method on an Eloquent model, what you're saying is essentially
Go through all subscriptions. For each one, start creating a SQL query. Unless that evaluates to false, keep the subscription in the list of all subscriptions.
Instead, what you mean to do is pretty close but not quite the same:
$test = $subscriptions->filter(function ($subscription) use ($id) {
return $subscription->description_id == $id;
});
Depending on what you want to do in practice, there may be other, more suitable methods as well. For instance, if you only want the first result since you only have a single id, then you can just replace filter above with first, and get the first matching subscription back rather than a collection of one subscription.

How to simplify this Laravel PHP code to one Eloquent query?

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.

If exist Update else Insert Into. MySQL - PHP

I'm asking this question in order to find the best practice to do it.
DB::table('owners')
->where('property_id',$id)
->update(array('person_id'=>$owner));
The problem is that in the table owners might not have a row to update. In that occasion i need to make an INSERT INTO instead of UPDATE.
My problem is that i have to run 2 queries each time, one for checking if the row already exists, and one more to update or insert into. Is it right to run 2 queries each time? Is there a better way to achieve that? I need to keep the queering processes fast for the user.
UPDATE: The table owners is a middle table of a many to many relationship. Unfortunately i cannot use ON DUPLICATE KEY.
well you could try to use firstOrCreate method of Laravel to check if user exists. After that retrieve the user object and pass it to an update function else if the user is not found firstOrCreate method will take care of you as it will create a new user with the data you will provide and will auto increment last user + 1 id.
There is also the option to use firstOrNew which will check if an instance exists based on the array values you passed and if no match is found it will auto create a new instance of the model you are handling for further manipulation.
Here is example with firstOrNew
Example Controller file.
public function getFirstUserOrNew($email)
{
$user = User::firstOrNew(['email' => $email]);
if($user)
{
$this->UpdateUser($user);
}
else
{
$this->CreateUser($user);
}
}
public function UpdateUser(User $user)
{
//Do update stuff
}
public function CreateUser(User $user)
{
//Do create stuff
}
P.S - I'm from Greece, if you want to discuss anything in native language send me a PM :)
EDIT:
Thanks to #Pyton contribution It seems you can also use an updateOrCreate method as it is explained here.
If you want to Update or Insert row You can use updateOrCreate
$owner = Owner::updateOrCreate(['property_id' => $id], ['person_id'=>$owner]);

In CodeIgniter, Can we use Database Transactions like this?

In CodeIgniter, Are we also allowed to use "Active Record Class" like this?
<?php
$data_balance = array(
'balance' => $newbalance
);
$data_transaction = array(
'amount' => $amount,
'user' => $user
);
$this->db->trans_start();
// This is an "Insert" query for a table
$this->db->set('date', 'NOW()', FALSE);
$this->db->insert('financial_transactions', $data_transaction);
// This is an "Update" query for another table
$this->db->where('user_id', $user_id);
$this->db->update('financial_balances', $data_balance);
$this->db->trans_complete();
if ($this->db->trans_status() === FALSE){
// Do stuff if failed
}
?>
Note that i use $this->db-> for both queries, so i don't know if the success result of first one is actually cleared to check the second one?
Is this going to work? can i trust that this will make either both queries to success or neither of them (i don't want one to success and one to fail)
From documentation:
By default CodeIgniter runs all transactions in Strict Mode. When strict mode is enabled, if you are running multiple groups of transactions, if one group fails all groups will be rolled back. If strict mode is disabled, each group is treated independently, meaning a failure of one group will not affect any others.

Categories