Lavavel/Eloquent 5 transactions rolling back not backing save - php

I do seem to be having problems getting database transactions to work on a model. I've referred to related posts on SO, but no luck yet.
In my example, I create a new record in the DB. I should be able to rollback and the new record should have disappeared shouldn't it?
try{
DB::beginTransaction();
$oNewMap = $oMap->replicate();
$oNewMap->name = "[test] " . $oNewMap->name;
$oNewMap->save(); // works
DB::rollBack(); / /record still in db
}
catch(\Exception $e){
DB::rollBack();
/* Transaction failed. */
}
When the rollback occured, why wouldn't the saved record disappear from the DB? Am I missing something with how models work with transactions?
The physical tables are all InnoDB, btw.
[EDITTED: to simplify the problem to a simple save and rollback, not doing two saves where the second save violates an FK constraint.]

If the model doesn't use the default database connection, you have to specify it on the transaction:
DB::connection('name')->beginTransaction();
DB::connection('name')->commit();
DB::connection('name')->rollBack();

Seeing your question, i remember a long time ago, where I had the same problem.
In the end i found out, that the function is called rollBack and not rollback - Note the capitalized B

Check your transaction level and conform that you are working with single beginTransaction() so, might you will find solution perfectly.
DB::beginTransaction()
DB::beginTransaction()
DB::transactionLevel() // will return 2
DB::commit() // doesn't commit
DB::transactionLevel() // will return 1
DB::commit() // finally, it commits to the database
DB::transactionLevel() // will return 0

Related

Laravel Transactions Not working when this condition happenes

Let me start with my code
On my Controller file this is the code
namespace Something\Somewhere\Controller{
class Mobile extends Controller{
public function saveMobilesIntoDb(Request $request,MediaManager $manager){
$requestMobileData = $request->all();
DB::beginTransaction();
try{
/*Do something*/
..
..
$bigMediaArray = $manager->mobileImagesManager($media,$insertedMobile,$mediaSlug);
..
..
DB::commit();
}catch (\Exception $exception) {
DB::rollback();
dump($exception);
}
}
}
}
Notice I am using a service there, The service is nothing but a namespace to manage the code in the service class this what happening
namespace Something\Somewhere\Service{
class MediaManager{
public function mobileImagesManager($media, $mobileId, $slug){
//Do some stuff
///Create folder
return array;
}
}
Now the issue is when I get some error in the service and I resend the data then suppose the last id inserted into database was 5 and then the error came but didn't rolled back so it saved the new id with 7 . and I don't want this to happen. I know the rollback is not working when I am not in the scope but what I tried so far is
I wrapped the service function into try-catch and in the catch I used the DB::rollback() but it didn't helped.
Please let me know how do I solve it and rollback everything when I am not in the scope.
Thank you for you time
As Alex said, due to Mysql official docs, the auto incremented ID will not rollback after transaction failure.
In all lock modes (0, 1, and 2), if a
transaction that generated
auto-increment values rolls back,
those auto-increment values are
“lost.” Once a value is generated for
an auto-increment column, it cannot be
rolled back, whether or not the
“INSERT-like” statement is completed,
and whether or not the containing
transaction is rolled back. Such lost
values are not reused. Thus, there may
be gaps in the values stored in an
AUTO_INCREMENT column of a table.

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.

Laravel - Must be inserted in two tables or otherwise it will fail

Does Laravel have some kind of functionality to allow me to insert row in Table-A but must be inserted in Table-B as well otherwise it will fail?
The Table-B table is polymorphic relationship.
As #C2486 stated in their answer, you should use transactions. However you do not need to use try and catch. In the database transactions section of the Laravel docs you can see the following example:
DB::transaction(function () {
DB::table('users')->update(['votes' => 1]);
DB::table('posts')->delete();
});
This method will automatically commit for you, and if an exception occurs during execution it will automatically rollback for you as well.
Use beginTransaction to sure to run all query.
DB::beginTransaction();
try {
// First query
// Second query
..........
DB::commit();
} catch (Exception $ex) {
DB::rollback();
}
For model you can check this answer

laravel migrations leave DB in an invalid state

If a migration fails half way through for any reason (E.g. typo), it commits half the migration, and leaves the rest out. It doesn't seem to try to roll back what it just did.(either by rolling back an encompassing transaction, or calling down())
If you try to manually rollback the last migration, e.g. php artisan migrate:rollback --step=1, it rolls back only the migration before last, i.e. the one before the one which failed.
Consider this migration:
public function up()
{
DB::table('address')->insert(['id'=>1,'street'=>'Demo', 'country_id'=>83]);
DB::table('customer')->insert(['id'=>1,'username'=>'demo','address_id'=>1]);
}
public function down()
{
DB::table('customer')->where('id',1)->delete();
DB::table('address')->where('id',1)->delete();
}
If the insert of the customer fails (e.g. we forgot to set a non null column, a typo, or a record exists when it should not), the address record WAS inserted.
migrate:rollback doesn't rollback this migration, it rolls back the one before, and we are left with a spurious orphaned address record. Obviously we can drop re-create the db and run the migration from scratch, but thats not the point - migrations should not leave half the migrations done and the DB in an invalid state.
Is there a solution? e.g. can one put transactions in the migration so it inserts all or nothing?
If we look in the migrations table after the half done migration has failed, it is not there.
NOTE: we use migrations to insert (and modify/delete) static data which the application requires to run. It is not dev data or test data. E.g. countries data, currencies data, as well as admin operators etc.
You should run these migrations inside a transaction:
DB::transaction(function () {
// Your code goes here.
}
or you can use a try/catch block:
try {
DB::beginTransaction();
// Your code goes here ...
DB::commit();
} catch(\Exception $e) {
DB::rollBack();
}

Foreign key and transaction

I'm trying to use transaction when creating table group, and table with relation user-group.
It works ok when I don't use transaction, so the naming of the attributes is correct. Here is the code:
$db = Yii::app()->db;
$transaction = $db->beginTransaction();
try {
$model->attributes=$_POST['MyGroup'];
$model->save();
$model->refresh();
$userMyGroup = new UserMyGroup();
$userMyGroup->IDMyGroup = $model->IDMyGroup;
$userMyGroup->IDUser = Yii::app()->user->id;
$userMyGroup->save();
$transaction->commit();
} catch (CDbException $ex) {
Yii::log("Couldn't create group:".$ex->errorInfo[1], CLogger::LEVEL_ERROR);
$transaction->rollback();
}
The error is:
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_UserMyGroup_MyGroup". The conflict occurred in database "MyDatabase", table "dbo.MyGroup", column 'IDMyGroup'.. The SQL statement executed was: INSERT INTO [dbo].[UserMyGroup] ([IDMyGroup], [IDUser]) VALUES (:yp0, :yp1). Bound with :yp0=4022, :yp1=1
Problem is probably that the saved model might not be in database while saving the second model(userMyGroup) with the foreign key. How to do the transaction correctly?
EDIT:
I've found out that the problem is caused by audit module, it is trying to log the query, but can't as it is in transaction and not really saved yet in database. I'm trying to figure out how to use this transaction along with the module...
The refresh method repopulates active record with the latest data.
While transaction is not commited latest data is existing data in table.
Move $model->refresh(); after $transaction->commit();
I've found out that the problem is caused by audit module which I'm using, it is trying to log the query, but can't as it is in transaction and not really saved yet in database. Unfortunately, I didn't figure out how to use this transaction along with the module, so the result is to disable audit module on the classes used in transaction.

Categories