Laravel Database Transaction for Data Consistency

Updated
featured-image.png

💡 I included the link to the example repository at the conclusion of this tutorial.

Laravel, a popular PHP framework, provides developers with a robust and elegant way to interact with databases. One crucial feature that aids in maintaining data integrity and consistency is the use of database transactions. In this article, we will explore the concept of Laravel database transactions and how they can be a valuable tool for developers, especially beginners, to ensure the reliability of their applications.

What is a Database Transaction?

In the context of databases, a transaction is a sequence of one or more SQL statements that are executed as a single unit of work. The primary goal of a transaction is to ensure the integrity and consistency of the data within the database. Laravel provides a convenient way to work with transactions through its database query builder.

Getting Started with Transactions in Laravel

Starting a Transaction

To initiate a database transaction in Laravel, you can use the beginTransaction method. This method is typically called on the database connection instance:

use DB;

DB::beginTransaction();

By invoking this method, Laravel will begin a new database transaction, allowing you to execute multiple queries as part of a single atomic operation.

Performing Operations

Once the transaction has started, you can perform various database operations, such as inserts, updates, or deletes, using Laravel’s query builder or Eloquent ORM. All these operations will be treated as part of the same transaction until you explicitly commit or roll back.

Ensuring Success: Committing a Transaction

After successfully executing all the necessary database operations, you can commit the transaction to make the changes permanent in the database. The commit method is used for this purpose:

DB::commit();

By calling this method, Laravel will persist all the changes made during the transaction to the database, ensuring data consistency.

Handling Errors: Rolling Back a Transaction

If an error occurs during the execution of the transaction or if certain conditions are not met, you can roll back the transaction to revert any changes made so far. The rollBack method is used for this purpose:

DB::rollBack();

This will undo any modifications made during the transaction, helping to maintain the integrity of the database in case of errors.

Example

Let’s consider a simple example where we want to transfer funds between two bank accounts using a transaction to ensure data consistency:

use DB;

DB::beginTransaction();

try {
    // Deduct amount from sender's account
    $senderAccount->decrement('balance', $amount);

    // Add amount to receiver's account
    $receiverAccount->increment('balance', $amount);

    // Commit the transaction
    DB::commit();
} catch (\Exception $e) {
    // Something went wrong, roll back the transaction
    DB::rollBack();
    // Handle the error, log, or return a response
}

Read also:

The DB::transaction() Method

Laravel simplifies the process of working with database transactions even further through the DB::transaction() method, which is a concise way to handle the beginning, committing, and rolling back of transactions. This method eliminates the need for manually calling beginTransaction(), commit(), and rollBack().

The DB::transaction() method is a higher-level abstraction provided by Laravel, making it easier for developers to work with database transactions. It ensures that the entire block of code within it is treated as a single transaction.

DB::transaction(function () {
    // Code to be executed within the transaction
});

Example Usage

Let’s revisit the fund transfer example using DB::transaction():

use DB;

DB::transaction(function () use ($senderAccount, $receiverAccount, $amount) {
    // Deduct amount from sender's account
    $senderAccount->decrement('balance', $amount);

    // Add amount to receiver's account
    $receiverAccount->increment('balance', $amount);
});

In this example, the anonymous function passed to DB::transaction() encapsulates the entire transaction logic. Laravel will automatically handle the starting of the transaction, committing it if the code within the block executes successfully, or rolling it back if an exception occurs.

Automatic Commit and Rollback

The DB::transaction() method automatically commits the transaction if the code within the block completes without throwing any exceptions. On the other hand, if an exception is thrown within the block, Laravel will catch it and automatically roll back the transaction, ensuring that the database remains in a consistent state.

Returning Values

If you need to return a value from the transaction block, you can use the return statement as usual. If an exception is thrown, the transaction will be rolled back, and the exception will propagate as expected.

use DB;

$result = DB::transaction(function () use ($senderAccount, $receiverAccount, $amount) {
    // Deduct amount from sender's account
    $senderAccount->decrement('balance', $amount);

    // Add amount to receiver's account
    $receiverAccount->increment('balance', $amount);

    // Return a success message or any other value
    return 'Funds transferred successfully';
});

More Example: Money Transfer Website

Let’s study a more interesting example to illustrate the implementation of DB::transaction() in a Laravel project. I’ll walk you through a step-by-step tutorial, providing complete code for a Laravel application designed to send and receive money. So, let’s dive in!

Step 1: Install Laravel

To begin, set up a fresh Laravel application. Create a new project specifically for this demonstration using one of the following methods:

Using the Laravel Installer

laravel new blog-laravel-db-transaction

Using Composer create-project

composer create-project laravel/laravel blog-laravel-db-transaction

Using Laravel build and Sail

curl -s "https://laravel.build/blog-laravel-db-transaction" | bash

Executing one of these commands will generate a new Laravel project within a directory named blog-laravel-db-transaction.

You can verify the success of your Laravel installation by executing the command php artisan serve and accessing localhost:8000 in your web browser.

Step 2: Database Setup

Create a database named blog_laravel_db_transaction, and update the .env file with your database configuration. For this tutorial, we’ll be using MySQL.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blog_laravel_db_transaction
DB_USERNAME=root
DB_PASSWORD=

This step ensures seamless integration with the database for our money transfer application.

Step 3: Update the users Table Migration

For this tutorial, our focus is on one table, namely users. Therefore, let’s simplify the migration file with the following adjustments:

// database\migrations\2014_10_12_000000_create_users_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        // Creating the 'users' table with columns: 'id', 'name', 'balance', 'timestamps'
        Schema::create('users', function (Blueprint $table) {
            $table->id(); // Auto-incremented primary key
            $table->string('name'); // Name of the user
            $table->unsignedBigInteger('balance'); // User's balance (unsigned to handle only positive values)
            $table->timestamps(); 
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        // Dropping the 'users' table if the migration needs to be rolled back
        Schema::dropIfExists('users');
    }
};

You can also delete other migration files.

Read also:

Step 4: Adjust the DatabaseSeeder File

Refine the DatabaseSeeder file with the following modifications:

// database\seeders\DatabaseSeeder.php

<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        // Creating two user records in the 'users' table with specified names and balances
        \App\Models\User::create([
            'name' => 'Fajar',
            'balance' => 150,
        ]);
        \App\Models\User::create([
            'name' => 'Stepen',
            'balance' => 100,
        ]);
    }
}

Step 5: Update the User Model

Update the $fillable attribute of the User model to allow writing to the name and balance columns:

// app\Models\User.php

// Allowing mass assignment for the 'name' and 'balance' column
protected $fillable = [
    'name',
    'balance',
];

Step 6: Executing the Migration and Seeder

Run the migration and the seeder by executing the following command:

php artisan migrate --seed

Step 7: Update the Welcome View

We will use the welcome page, so we need to update it by writing some codes. Update the welcome page by inserting a form that includes a simple flash notification, information about the sender’s balance, recipient’s balance, an input field for the amount of money to be sent, and a send button.

<!-- resources\views\welcome.blade.php -->

<!-- head code -->
<!-- ... -->

<body class="antialiased">
    <div class="relative sm:flex sm:justify-center sm:items-center min-h-screen bg-dots-darker bg-center bg-gray-100 dark:bg-dots-lighter dark:bg-gray-900 selection:bg-red-500 selection:text-white">
        <div class="max-w-7xl mx-auto p-6 lg:p-8">
            @if ($notif = Session::get('notif'))
            <div style="color: red">{{ $notif }}</div>
            @endif
            <div>
                <div>Sender balance</div>
                <div>{{ $senderBalance }}</div>
                <div>Receiver balance</div>
                <div>{{ $receiverBalance }}</div>
            </div>
            <div>
                <form action="/send-to-receiver" method="POST">
                    @csrf
                    <input type="number" name="amount" value="{{ old('amount') }}">
                    <div>
                        <button type="submit"><strong>Send to receiver</strong></button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</body>

<!-- ... -->

Step 8: Create a Route to Send the Money

Create a route to facilitate the money transfer from the receiver.

// routes\web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HomeController;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "web" middleware group. Make something great!
|
*/

Route::get('/', [HomeController::class, 'index']);
// handling the process of sending money to the receiver.
Route::post('/send-to-receiver', [HomeController::class, 'sendToReceiver']);

Step 9: Creating a HomeController

Let’s create a controller to display our form and send the money from sender to receiver. This is a good feature to demonstrate the DB::transaction() utilization and how to use it with the try-catch codes.

// app\Http\Controllers\HomeController.php
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;

class HomeController extends Controller
{
    /**
     * Display the form and sender/receiver balances.
     */
    public function index() {
        // Retrieve the balance of the first user as the sender
        $senderBalance = User::first()->balance;
        
        // Retrieve the balance of the last user as the receiver
        $receiverBalance = User::orderByDesc('id')->first()->balance;

        // Pass the balances to the welcome view
        return view('welcome', [
            'senderBalance' => $senderBalance,
            'receiverBalance' => $receiverBalance,
        ]);
    }

    /**
     * Process the money transfer from sender to receiver.
     */
    public function sendToReceiver() {
        $amount = (int) request()->amount;
        $sender = User::first();
        $receiver = User::orderByDesc('id')->first();
        
        try {
            \DB::transaction(function () use ($amount, $sender, $receiver) {
                //If the amount to be sent is more than the sender's balance, 
                //It will throw an error and no balance will change.
                //You can catch the SQL error with your error-handling

                // Deduct the amount from the sender's balance
                $sender->update(['balance' => $sender->balance - $amount]);
                
                // for example, we have a requirement 
                // that the receiver's balance should not exceed 200

                // Check if the receiver's balance exceeds 200 after the transfer
                if ($receiver->balance + $amount > 200) {
                    // We can throw an error inside the transaction and catch it outside the transaction
                    // Later you will notice that after Laravel displays this error, no balance has changed
                    throw new \Exception('Receiver\'s balance should not exceed 200');
                }

                // Update the receiver's balance
                $receiver->update(['balance' => $receiver->balance + $amount]);
            });
        } 
        catch (\Exception $e) {
            // Handle exceptions and display a notification
            return back()->withInput()->with('notif', $e->getMessage());
        }

        // Redirect back after a successful transfer
        return back();
    }
}

Read also:

Step 10: Let’s Test Our App

We can test our app by running the local development server

php artisan serve
money-transfer-app.png

Visit the / page and test the form.

  1. If you enter 150 and send the money, it will return the “Receiver’s balance should not exceed 200” error receiver-error.png
  2. If you enter 200, Laravel will give you an error from MySQL sender-error.png
  3. No balance changed when the error occurred
  4. If you enter 50, then the transaction will be successful

Conclusion

In this tutorial, we explored the practical implementation of Laravel’s DB::transaction() feature, a powerful tool for ensuring data consistency and integrity within database operations. The example scenario of a money transfer application demonstrated how transactions can be utilized to handle complex processes involving multiple database interactions.

The use of a try-catch block within the transaction enables developers to handle exceptions gracefully. This is especially valuable when dealing with complex scenarios, such as checking for balance constraints or other business rules.

By following the steps outlined in this tutorial, developers, especially beginners, can gain a deeper understanding of how to leverage Laravel’s database transactions to build robust and reliable applications. The provided example serves as a foundation for incorporating these principles into real-world projects, promoting best practices in database management and application development.

💻 The repository for this example can be found at fajarwz/blog-laravel-db-transaction.

Fajarwz's photo
Fajar Windhu Zulfikar

I'm a full-stack web developer who loves to share my software engineering journey and build software solutions to help businesses succeed.

Email me
Ads
  • Full-Stack Laravel: Forum Web App (Complete Guide 2024)
  • Flexible and powerful review system for Laravel, let any model review and be reviewed.

Share

Support

Subscribe

Sign up for my email newsletter and never miss a beat in the world of web development. Stay up-to-date on the latest trends, techniques, and tools. Don't miss out on valuable insights. Subscribe now!

Comments

comments powered by Disqus