The Eloquent ORM is quite nice, though I'm wondering if there is an easy way to setup MySQL transactions using innoDB in the same fashion as PDO, or if I would have to extend the ORM to make this possible?
You can do this:
DB::transaction(function() {
//
});
Everything inside the Closure executes within a transaction. If an exception occurs it will rollback automatically.
If you don't like anonymous functions:
try {
DB::connection()->pdo->beginTransaction();
// database queries here
DB::connection()->pdo->commit();
} catch (\PDOException $e) {
// Woopsy
DB::connection()->pdo->rollBack();
}
Update: For laravel 4, the pdo object isn't public anymore so:
try {
DB::beginTransaction();
// database queries here
DB::commit();
} catch (\PDOException $e) {
// Woopsy
DB::rollBack();
}
If you want to use Eloquent, you also can use this
This is just sample code from my project
/*
* Saving Question
*/
$question = new Question;
$questionCategory = new QuestionCategory;
/*
* Insert new record for question
*/
$question->title = $title;
$question->user_id = Auth::user()->user_id;
$question->description = $description;
$question->time_post = date('Y-m-d H:i:s');
if(Input::has('expiredtime'))
$question->expired_time = Input::get('expiredtime');
$questionCategory->category_id = $category;
$questionCategory->time_added = date('Y-m-d H:i:s');
DB::transaction(function() use ($question, $questionCategory) {
$question->save();
/*
* insert new record for question category
*/
$questionCategory->question_id = $question->id;
$questionCategory->save();
});
If you want to avoid closures, and happy to use facades, the following keeps things nice and clean:
try {
\DB::beginTransaction();
$user = \Auth::user();
$user->fill($request->all());
$user->push();
\DB::commit();
} catch (Throwable $e) {
\DB::rollback();
}
If any statements fail, commit will never hit, and the transaction won't process.
I'm Sure you are not looking for a closure solution, try this for a more compact solution
try{
DB::beginTransaction();
/*
* Your DB code
* */
DB::commit();
}catch(\Exception $e){
DB::rollback();
}
For some reason it is quite difficult to find this information anywhere, so I decided to post it here, as my issue, while related to Eloquent transactions, was exactly changing this.
After reading THIS stackoverflow answer, I realized my database tables were using MyISAM instead of InnoDB.
For transactions to work on Laravel (or anywhere else as it seems), it is required that your tables are set to use InnoDB
Why?
Quoting MySQL Transactions and Atomic Operations docs (here):
MySQL Server (version 3.23-max and all versions 4.0 and above) supports transactions with the InnoDB and BDB transactional storage engines. InnoDB provides full ACID compliance. See Chapter 14, Storage Engines. For information about InnoDB differences from standard SQL with regard to treatment of transaction errors, see Section 14.2.11, “InnoDB Error Handling”.
The other nontransactional storage engines in MySQL Server (such as MyISAM) follow a different paradigm for data integrity called “atomic operations.” In transactional terms, MyISAM tables effectively always operate in autocommit = 1 mode. Atomic operations often offer comparable integrity with higher performance.
Because MySQL Server supports both paradigms, you can decide whether your applications are best served by the speed of atomic operations or the use of transactional features. This choice can be made on a per-table basis.
If any exception occurs, the transaction will rollback automatically.
Laravel Basic transaction format
try{
DB::beginTransaction();
/*
* SQL operation one
* SQL operation two
..................
..................
* SQL operation n */
DB::commit();
/* Transaction successful. */
}catch(\Exception $e){
DB::rollback();
/* Transaction failed. */
}
The best and clear way:
DB::beginTransaction();
try {
DB::insert(...);
DB::insert(...);
DB::insert(...);
DB::commit();
// all good
} catch (\Exception $e) {
DB::rollback();
// something went wrong
}
Related
The Eloquent ORM is quite nice, though I'm wondering if there is an easy way to setup MySQL transactions using innoDB in the same fashion as PDO, or if I would have to extend the ORM to make this possible?
You can do this:
DB::transaction(function() {
//
});
Everything inside the Closure executes within a transaction. If an exception occurs it will rollback automatically.
If you don't like anonymous functions:
try {
DB::connection()->pdo->beginTransaction();
// database queries here
DB::connection()->pdo->commit();
} catch (\PDOException $e) {
// Woopsy
DB::connection()->pdo->rollBack();
}
Update: For laravel 4, the pdo object isn't public anymore so:
try {
DB::beginTransaction();
// database queries here
DB::commit();
} catch (\PDOException $e) {
// Woopsy
DB::rollBack();
}
If you want to use Eloquent, you also can use this
This is just sample code from my project
/*
* Saving Question
*/
$question = new Question;
$questionCategory = new QuestionCategory;
/*
* Insert new record for question
*/
$question->title = $title;
$question->user_id = Auth::user()->user_id;
$question->description = $description;
$question->time_post = date('Y-m-d H:i:s');
if(Input::has('expiredtime'))
$question->expired_time = Input::get('expiredtime');
$questionCategory->category_id = $category;
$questionCategory->time_added = date('Y-m-d H:i:s');
DB::transaction(function() use ($question, $questionCategory) {
$question->save();
/*
* insert new record for question category
*/
$questionCategory->question_id = $question->id;
$questionCategory->save();
});
If you want to avoid closures, and happy to use facades, the following keeps things nice and clean:
try {
\DB::beginTransaction();
$user = \Auth::user();
$user->fill($request->all());
$user->push();
\DB::commit();
} catch (Throwable $e) {
\DB::rollback();
}
If any statements fail, commit will never hit, and the transaction won't process.
I'm Sure you are not looking for a closure solution, try this for a more compact solution
try{
DB::beginTransaction();
/*
* Your DB code
* */
DB::commit();
}catch(\Exception $e){
DB::rollback();
}
For some reason it is quite difficult to find this information anywhere, so I decided to post it here, as my issue, while related to Eloquent transactions, was exactly changing this.
After reading THIS stackoverflow answer, I realized my database tables were using MyISAM instead of InnoDB.
For transactions to work on Laravel (or anywhere else as it seems), it is required that your tables are set to use InnoDB
Why?
Quoting MySQL Transactions and Atomic Operations docs (here):
MySQL Server (version 3.23-max and all versions 4.0 and above) supports transactions with the InnoDB and BDB transactional storage engines. InnoDB provides full ACID compliance. See Chapter 14, Storage Engines. For information about InnoDB differences from standard SQL with regard to treatment of transaction errors, see Section 14.2.11, “InnoDB Error Handling”.
The other nontransactional storage engines in MySQL Server (such as MyISAM) follow a different paradigm for data integrity called “atomic operations.” In transactional terms, MyISAM tables effectively always operate in autocommit = 1 mode. Atomic operations often offer comparable integrity with higher performance.
Because MySQL Server supports both paradigms, you can decide whether your applications are best served by the speed of atomic operations or the use of transactional features. This choice can be made on a per-table basis.
If any exception occurs, the transaction will rollback automatically.
Laravel Basic transaction format
try{
DB::beginTransaction();
/*
* SQL operation one
* SQL operation two
..................
..................
* SQL operation n */
DB::commit();
/* Transaction successful. */
}catch(\Exception $e){
DB::rollback();
/* Transaction failed. */
}
The best and clear way:
DB::beginTransaction();
try {
DB::insert(...);
DB::insert(...);
DB::insert(...);
DB::commit();
// all good
} catch (\Exception $e) {
DB::rollback();
// something went wrong
}
I have multiple PDO objects, referring to different databases on different servers. I need to be able to rollback all transactions in case of error at any step.
In fact, I want to have something like this:
try {
$db1Connector->beginTransaction();
$db2Connector->beginTransaction();
//some functionality
$db1Connector->commit();
$db2Connector->commit();
} catch (\Exception $exception) {
$db1Connector->rollback();
$db2Connector->rollback();
}
I have this functionality in my code, but it seems it does not work correctly. I noticed some cases that only one of the transactions is rollbacked. I guess this problem occurs when $db1Connector->commit() or $db2Connector->commit() fails.
Yes, it is possible. You need to use a distributed transaction. See https://stackoverflow.com/questions/17772363/distributed-transaction-on-mysql#:~:text=so%20server%20A%20sends%20a,server%20A%20and%20server%20B.
I need to run a query outside the transaction that I have started:
$transaction = \Yii::$app->db->beginTransaction();
try {
//... other database queries within the transaction ...
//Query I want to be inserted regardless:
\Yii::$app->db->createCommand("INSERT INTO...")->execute();
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollBack();
throw $e;
}
Is there a smart way to do that, or do I need to clone/create a new database connection - and if so, what's the best way to do that without having to specify the database parameters again and just use the same config?
You need to use separate connection or move this query after/before transaction.
For copying DB component you can simply use clone - new instance should open new connection on first query:
$connection = clone Yii::$app->db;
$connection->createCommand("INSERT INTO...")->execute();
It is used in this way in yii\log\DbTarget.
But you may consider declaring separate DB component for this task (Yii::$app->db2) - then you will be able to reuse this additional connection.
A few weeks back I had an interesting inquiry regarding the handling/processing of an inventory/payment system and I've decided to implement the method of the accepted answer being it was both the most simplistic and more likely the most efficient solution given.
I'm using Laravel's Eloquent ORM in conjunction with the Slim Framework, and I'd like to perform a MYSQL transaction with a specific query regarding the decrementation of the stock of items within an inventory system. The code that I'd like to use a transaction for is the following:
// decrement each items stock following successful payment
try {
// here is where i'd like to perform a transaction, rather than simple eloquent query
foreach ($this->c->basket->all() as $product) {
$product->decrement('stock', $product->quantity);
}
} catch (Exception $e) {
// if error is caused during decremention of item(s),
// handle accordingly
}
Now being that I am using Slim as aforementioned, I believe I don't have native access to the Laravel DB Facade which to my knowledge handles transactions.
Suggestions?
SIDE-NOTE
My DB Connection is set globally in an initialization file like so:
$capsule = new Illuminate\Database\Capsule\Manager();
$capsule->addConnection($container['settings']['db']);
$capsule->setAsGlobal();
$capsule->bootEloquent();
You could get the Capsule object from the slim container and then just use the Connection object to start and commit a database transaction. If an exception occurs you just need to call the rollBack() method.
Pseudo code:
$capsule = new \Illuminate\Database\Capsule\Manager();
// Get a registered connection instance.
$connection = $capsule->getConnection();
// begin a transaction manually
$connection->beginTransaction();
try {
// do something...
$product->decrement('stock', $product->quantity);
// commit a transaction via the commit method
$connection->commit();
} catch (Exception $e) {
// you can rollback the transaction via the rollBack method
$connection->rollBack();
}
I have a mobile application and server based on Symfony which gives API for the mobile app.
I have a situation, where users can like Post. When users like Post I add an entry in ManyToMany table that this particular user liked this particular Post (step 1). Then in Post table I increase likesCounter (step 2). Then in User table I increase gamification points for user (because he liked the Post) (step 3).
So there is a situation where many users likes particular Post at the same time and deadlock occurs (on Post table or on User table).
How to handle this? In Doctrine Docs I can see solution like this:
<?php
try {
// process stuff
} catch (\Doctrine\DBAL\Exception\RetryableException $e) {
// retry the processing
}
but what should I do in catch part? Retry the whole process of liking (steps 1 to 3) for instance 3 times and if failed return BadRequest to the mobile application? Or something else?
I don't know if this is a good example cause maybe I could try to rebuild the process so the deadlock won't happen but I would like to know what should I do if they actually happen?
I disagree with Stefan, deadlocks are normal as the MySQL documentation says:
Normally, you must write your applications so that they are always prepared to re-issue a transaction if it gets rolled back because of a deadlock.
See: MySQL documentation
However, the loop suggested by Stefan is the right solution. Except that it lacks an important point: after Doctrine has thrown an Exception, the EntityManager becomes unusable and you must create a new one in the catch clause with resetManager() from the ManagerRegistry instance.
When I had exactly the same concern as you, I searched the web but couldn't find any completely satisfactory answer. So I got my hands dirty and came back with an article where you'll find an implementation exemple of what I said above:
Thread-safe business logic with Doctrine
What I'd do is post all likes on a queue and consume them using a batch consumer so that you can group the updates on a single post.
If you insist on keeping you current implementation you could go down the road you yourself suggested like this:
<?php
for ($i = 0; $i < $retryCount; $i++) {
try {
// try updating
break;
} catch (\Doctrine\DBAL\Exception\RetryableException $e) {
// you could also add a delay here
continue;
}
}
if ($i === $retryCount) {
// throw BadRequest
}
This is an ugly solution and I wouldn't suggest it. Deadlocks shouldn't be "avoided" by retrying or using delays. Also have a look at named locks and use the same retry system, but don't wait for the deadlock to happen.
The problem is that after Symfony Entity Manager fails - it closes db connection and you can't continue you work with db even if you catch the ORMException.
First good solution is to process your 'likes' async, with rabbitmq or other queue implementation.
Step-by-step:
Create message like {type: 'like', user:123, post: 456}
Publish it in queue
Consume it and update 'likes' count.
You can have several consumers that try to obtain lock on based on postId. If two consumers try to update same post - one of them will fail obtaining the lock. But it's ok, you can consume failed message after.
Second solution is to have special table e.g. post_likes (userId, postId, timestamp). Your endpoint could create new rows in this table synchronously. And you can count 'likes' on some post with this table. Or you can write some cron script, which will update post likes count by this table.
I've made a special class to retry on deadlock (I'm on Symfony 4.4).
Here it is :
class AntiDeadlockService
{
/**
* #var EntityManagerInterface
*/
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function safePush(): void
{
// to retry on deadlocks or other retryable exceptions
$connection = $this->em->getConnection();
$retry = 0;
$maxRetries = 3;
while ($retry < $maxRetries) {
try {
if (!$this->em->isOpen()) {
$this->em = $this->em->create(
$connection = $this->em->getConnection(),
$this->em->getConfiguration()
);
}
$connection->beginTransaction(); // suspend auto-commit
$this->em->flush();
$connection->commit();
break;
} catch (RetryableException $exception) {
$connection->rollBack();
$retry++;
if ($retry === $maxRetries) {
throw $exception;
}
}
}
}
}
Use this safePush() method instead of the $entityManager->push() one ;)