one easy fix to make your database logic more stable.

August 20, 2021

Laravel is a time-tested framework with more features than any one person will use in any standard project. That said, every now and again, I come across useful bits of framework that have been in the codebase for years–this is one of those bits.

The Problem

We've all run into this before: you've built an application where one table in your database architecture relies on one or more tables in your database. When you go to do work on one of those related tables, you also have to do some sort of work on the others as well. The "happy path" is easy: change your target table, then subsequently change all of the data that your target table relies on. Voila! Problem solved.

Or is it?

This exact issue has gotten me on more than one occasion. What happens if any one of those data changes errors out?

The Solution

I was poking around in the Laravel Jetstream (Livewire edition) source code the other day, and I noticed a static method on the DB facade that I hadn't thought about in a long time (since Laravel 5.7, if I remember right). In the delete() method on the DeleteUser action, the Laravel team has added the DB::transaction() method as a wrapper around the other logic. Here's the code block to reference as we keep moving:

public function delete($user)
 {
    DB::transaction(function () use ($user) {
        $this->deleteTeams($user);
        $user->deleteProfilePhoto();
        $user->tokens->each->delete();
        $user->delete();
    });
}

So what does this DB::transaction() method do exactly?

For the most part, the code within the closure function runs exactly like it would if it were placed in any other location within your app. However, there is one special feature that you get when using the transaction method: rollbacks.

Like I mentioned earlier in the problem statement, if we were to run these queries back to back, but one of them had an error, we'd be left in a weird limbo state where some of the queries had run, one had errored, and all the queries after the error never ran. Using this delete() method as an example, if it weren't wrapped in the DB::transaction() method, if the $user->deleteProfilePhoto() database query failed, then we would have the following output:

  • The User's teams would have been deleted
  • The User's profile photo would have failed to have been deleted
  • The User's tokens would not have been removed (because the code didn't get there)
  • The User themself wouldn't have been deleted (!)

Not an ideal scenario to say the least.

Now, since this code is wrapped in the DB::transaction() method, at any point, were a database query to fail, all the previous queries would be rolled back. This means that, in our previous scenario, if the $user->deleteProfilePhoto() method was to fail, instead of the User's teams being deleted and the User's tokens and the User themself being in limbo, everything that had happened up to that point (i.e. the User's teams being deleted) would be reversed.

Great, right?

There are so many situations where this can be put to good use, but I think that this Jetstream example is one of the best.

What other situations could you use the DB::transaction() method in your Laravel applications? Reach out to me on Twitter at [@alexandersix_][(https://twitter.com/alexandersix_) and let me know! You know I'll be putting this in a lot more of my applications going forward.