Laravel: A Database Transaction to rule them all
Do all, or do nothing
Sometimes you need to do a series of SQL queries, and every one of these are so tied together that, if one fails for whatever reason, st*t can happen.
Just picture this:
/**
* Complete the registration of the User
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function completeRegister(Request $request)
{
$validated = $request->validate([
// Bunch of rules
]); $user = User::create($validated); $this->createDefaultPreferences($user); // Get the API keys from an external service through Internet
$keys = ExternalService::getKeysForUser($user->id); $user->keys()->create([ 'keys' => $keys ]); return response()->view('user-created', ['user' => $user]);
}
And let’s say the External Service fails miserably, because it’s down, and the keys are not retrieved nor saved. An error occurs, and the user is registered without keys, and can’t use the site. Now you have to retrieve them manually, or create a mechanism to get the keys.
You could say “Hey, put the keys thing first before proceeding to save the user”, “ping the service”, or “make it so the keys aren’t a requirement”, which are totally valid, but I want to make an example of something that could fail. Even the database can do sometimes.
For these problems, the transactions are for.
Transactions: saving your database, one statement at a time
Transactions are a incredible nice feature of SQL RDBMS like MySQL, MariaDB, PostgreSQL and SQL Server. It allows the database to “rollback” a whole set of statements. I will let the documentation speak for itself:
You may use the
transaction
method on theDB
facade to run a set of operations within a database transaction. If an exception is thrown within the transactionClosure
, the transaction will automatically be rolled back. If theClosure
executes successfully, the transaction will automatically be committed. You don't need to worry about manually rolling back or committing while using thetransaction
method.
So, let’s solve our keys problem using a transaction.
DB::transaction(function() use ($validated) { $user = User::create($validated); $this->createDefaultPreferences($user); // Get the API keys from an external service
$keys = ExternalService::getKeysForUser($user->id); $user->keys()->create([ 'keys' => $keys ]);});
If the ExternalService
, which does an HTTP Request through the Internet, fails doing what it must, an Exception
will be thrown. When that happens, the whole transaction will be “rolled back”, like nothing happened.
This happens because the transaction
method uses a try catch
block. If an exception is catched, it will rollback the transactions automatically before proceeding to throw the exception.
But hey, I want to try X number of times!
Thankfully, the transaction accepts a second argument, which is the number of retries. If the External Service can’t connect because of heavy load, we can try again 3 more times.
DB::transaction(function() { // ... all over again}, 3); // Retry 3 more times before failing miserably
What if the External Service is DEAD?
Let’s say the External Service is under DDoS attack, someone couldn’t pay the electricity bill because it used it all his money buying levels for his Dota 2 Battle Pass, is just down for maintenance, or whatever event that blocks us for reaching the service. We have depleted our tries, so what we can do? We can catch the exception.
try {
DB::transaction(function() { // ... all over again }, 3); // Retry 3 more times before failing miserably
} catch (ExternalServiceException $exception) { return 'Sorry, the External Service is down and you cannot complete the registration without the keys from it'.}
As said before, if the transaction fails, it will return whichever Throwable
was thrown first by the code inside the Closure.
By the way, you don’t need to use the try catch
block. Laravel has a nice exception handling, and the report and render methods to do what their names imply.
For example, if your External Service throws a particular exception, you can render it to something the user can understand, like “The service is down, click here to remind you to register when it’s up”, and use the report method to send you an email and see what the hell happened.
So, yeah, no need to hack your controllers with walls of code.