Laravel: A Database Transaction to rule them all

Do all, or do nothing

Italo Baeza Cabrera
3 min readJun 3, 2019
Photo by Tim Gouw on Unsplash

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 the DB facade to run a set of operations within a database transaction. If an exception is thrown within the transaction Closure, the transaction will automatically be rolled back. If the Closure executes successfully, the transaction will automatically be committed. You don't need to worry about manually rolling back or committing while using the transaction 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.

--

--

Italo Baeza Cabrera
Italo Baeza Cabrera

Written by Italo Baeza Cabrera

Graphic Designer graduate. Full Stack Web Developer. Retired Tech & Gaming Editor. https://italobc.com

Responses (3)