Laravel : Unable to insert in database after a transaction has rolled back - laravel

I successfully use transaction in Laravel to this point : I cannot execute another query after transaction has rolled back.
DB::beginTransaction();
try {
// DO some stuff
// Commit transaction
DB::commit();
} catch (\Exception $e) {
DB::rollback();
// This statement has no effect
DB:insert('something');
}
If I execute DB:insert('something'); just before the transaction, it works fine.
Anything I do wrong ?
No error is thrown. The DB::insert just does not insert anything in the database.

The problem came from elsewhere, sorry for the inconvenience.
For the record, be careful if you use a model instance in your transaction and want to use it after the rollback.
$myModel = MyModel::find(1);
$myModel->someData = 'model to delete';
$myModel->save();
DB::beginTransaction();
try {
$myModel->delete();
DB::commit();
} catch (\Exception $e) {
DB::rollback();
// This won't work : tries to make an insert
$myModel->someData = 'some data';
$myModel->save();
// This will work : will update the instance
$myModel = MyModel::find(1);
$myModel->someData = 'some data';
$myModel->save();
}

Related

Transaction in Laravel rolled back incompletely

I have an array of Organization model. Each Organization contains an array of User model.
There is a many-to-many relationship between them using the OrganizationUser model.
If I have problems inserting an Organization, an User, or a relationship between them, I want to roll everything back.
But I also use "try catch" blocks to see at which stage the error occurred.
To test this, I explicitly made a mistake in the OrganizationUser relationship object. I'm using an User id that can't be in the Organization.
This rolls back the Organization, relationships, and first User, but all other Users are added to the database.
DB::beginTransaction();
$org->save();
foreach ($org->user_list as $user) {
try {
$user->save();
} catch (\Exception $exception) {
DB::rollback();
Log::error('Error user insert : '. $user->inn .' '. $exception->getMessage());
}
try {
$orgs_user = OrganizationUser::create([
'user_id' => 80,
'org_id' => $org->id,
]);
} catch (\Exception $exception) {
DB::rollback();
Log::error('Insert user_org relationship : ' . $exception->getMessage());
}
}
When I didn't use the "try catch" block it worked well
When you call DB::rollBack() (by the way it's rollBack with a capital B), that ends the transaction. The rest of the queries will execute as normal.
The safest way to use transactions is to put the entire thing in a single try catch. The \Exception itself should contain enough information to know what caused it in its message or stack trace.
try {
DB::beginTransaction();
$org->save();
foreach ($org->user_list as $user) {
$user->save();
$orgs_user = OrganizationUser::create([
'user_id' => 80,
'org_id' => $org->id,
]);
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
}
It is a bit verbose, but you can also use the DB::transaction() method.
DB::transaction(function () use ($org) {
$org->save();
foreach ($org->user_list as $user) {
$user->save();
$orgs_user = OrganizationUser::create([
'user_id' => 80,
'org_id' => $org->id,
]);
}
});
DB::rollback does not stop the execution of the code.
If you are in a catch block the transaction will be rolled back but the next block (as well as the rest of the iteration) will continue.
Use a break; after the rollback to break out of the loop completely like below:
DB::beginTransaction();
$org->save();
foreach ($org->user_list as $user) {
try {
$user->save();
} catch (\Exception $exception) {
DB::rollback();
Log::error('Error user insert : '. $user->inn .' '. $exception->getMessage());
break;
}
try {
$orgs_user = OrganizationUser::create([
'user_id' => 80,
'org_id' => $org->id,
]);
} catch (\Exception $exception) {
DB::rollback();
Log::error('Insert user_org relationship : ' . $exception->getMessage());
break;
}
}
You can also return; or just exit() depending on how you want to handle the error

DB Transaction not rolling back [Laravel 7.3]

I'm running Laravel 7.3 on an app, and I feel like this is potentially a Laravel framework bug because I'm racking my brain on this...
I've got a DB Transaction with model saves within it, all within a try/catch but it's still saving the model updates, and I just can't see why because it hits the catch() and the response json hits, so I assume the rollback is running, yet the database is still updating.
try {
DB::beginTransaction();
foreach ($models as $model) {
$model->rungroup_id = $rungroup->id;
$model->zone_id = $rungroup->getFirstZoneID();
$model->delivery_status = 'next';
$model->save();
}
DB::commit();
return response()->json(['status' => 'success']);
} catch (Throwable | Exception $e) {
DB::rollBack();
Log::error($e);
return response()->json([
'status' => 'error',
'message' => $e->getMessage(),
], 500);
}
Any ideas?
After updating Laravel and many other attempts at rewriting the code to see if I made a silly mistake, I ended up finding that the person who created the database tables set the Table engine to MyISAM rather than InnoDB which is required for the DB transactions... I wish Laravel threw an error so I would've known, but this has definitely saved my sanity, and hope it helps others.

Save two new models with a relationship to each other at once in Laravel 6

I'm wondering what's the best way to save two different models with a relationship to each other at once. Consider I have a Subscription model and a Participant model, and when I create a Participant, it has to create a Subscription and link the Participant to it.
Example:
class Subscription extends Model
{
public function participants()
{
return $this->hasMany('App\Participant');
}
}
class Participant extends Model
{
public function subscription()
{
return $this->belongsTo('App\Subscription');
}
}
Saving:
$s = new App\Subscription();
$p = new App\Participant();
$s->participants()->save($p);
But then the Subscription isn't saved. Any ideas what's the best practice to save them both, check if they're saved and make the relation?
What you're looking for is transactions. You can do them 2 ways.
Using DB::transaction() and putting everything in a Closure
use DB;
$s = new App\Subscription();
$p = new App\Participant();
DB::transaction(function () use ($s, $p) {
// If something fails, it will rollBack, if not, it will commit.
$s->save();
$s->participants()->save($p);
});
Or manually, where you control when it's commited or rollback'd.
use DB;
$s = new App\Subscription();
$p = new App\Participant();
try {
DB::beginTransaction();
$s->save();
$s->participants()->save($p);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
}
More info on transactions
You can do that with transactions (as in #IGP's answer), or just add saving line for subscriptions too..
public function test()
{
// Begin Transaction
DB::beginTransaction();
try
{
$participant = new Participant();
$subscription = new Subscription();
// $subscription->name = "parent subscription"; // init necessary columns
$subscription->save();
$participant->subscription()->associate($subscription);
// $participant->name = "child participant"; // init necessary columns
$participant->save();
// Commit Transaction
DB::commit();
// Continue your logic here
} catch (\Exception $e) {
// Rollback Transaction
DB::rollback();
return $e->getMessage();
}
}

Laravel transaction saves data into DB even when it's rollbacked

I send a request, trying to save multiple documents at once.
Here is my code
DB::beginTransaction();
try {
foreach ($request->documents as $index => $documentInfo) {
// Check
if (// some statement) {
$documentExists = Document::where([
// some checks
])
->exists();
if ($documentExists) {
throw new \Exception("Error Processing Request", 1);
}
}
// Assign document properties
$document->save();
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
if ($request->ajax()) {
return response()->json([
'success' => false,
'document' => $index,
'message' => '// message',
]);
}
return redirect()->back()->with('error', '// message');
}
The thing that happens is that I check every document for its own unique values. If two documents with same values try to get saved I want to return an error.
I tried uploading two documents with the same values, the error is returned on the second document and the transaction must fail, but the first document gets saved into the database.
I don't want this, if there is an error I don't want any documents saved into the DB.

Return Date To Client From Within DB::Transaction Closure()

I am executing several database saves within my db transaction closure:
DB::transaction(function() {
...
});
However what i want to do now is when a transaction fails instead of throwing an exception i want to return a custom JSON object straight t the client, if the transactions succeeds i want to do the same thing.
This is my object:
return [
'code' => '',
'message' => '',
'data' => []
];
How would i return the above to the client from within the closure?
Instead of DB::transaction Closure you can use DB::beginTransaction, DB::commit and DB::rollback methods in order to have more control in code, you can wrap your DB actions like this :
DB::beginTransaction();
try {
DB::insert(...);
DB::insert(...);
DB::insert(...);
//If everything is ok we commit
DB::commit();
return response()->json(["status" => "success"])
} catch (Exception $e) {
//something goes wrong, we rollback
DB::rollback();
return response()->json(["error" => "Some error"]);
}
You can research more in database transaction docs

Resources