Use MySQL Transactions in PHP on Ubuntu 20.04

Updated on November 13, 2020
Use MySQL Transactions in PHP on Ubuntu 20.04 header image

Introduction

A MySQL transaction is a group of SQL (Structured Query Language) commands that you execute on your database as a single unit. You must use MySQL transactions in all applications that require ACID (Atomicity, Consistency, Isolation, and Durability) compliance. For instance, in a bank application, transfers of money from one account to another must be done using MySQL transactions. This ensures that one account is debited and the other is credited in an atomic way. If either of the commands fails, the entire transaction must be rolled back before changes are permanently committed to disk. In this guide, you'll implement MySQL transactions with PHP on a Vultr Ubuntu 20.04 cloud server.

Prerequisites

Before you begin, ensure that you've the following:

1. Create a Test Database

SSH to your server and login to MySQL as a root user.

$ sudo mysql -u root -p

Type your MySQL root password and press Enter to proceed.

Create a bank_db database.

mysql> CREATE DATABASE bank_db;

Create a bank_user with the right privileges to access the bank_db database. Use a strong value for the EXAMPLE_PASSWORD to prevent brute-force attacks.

mysql> CREATE USER 'bank_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'EXAMPLE_PASSWORD';
mysql> GRANT ALL PRIVILEGES ON bank_db.* TO 'bank_user'@'localhost';           
mysql> FLUSH PRIVILEGES;

Select the bank_db database.

mysql> USE bank_db;

Start by creating a savings_accounts table.

mysql> CREATE TABLE savings_accounts (
       account_id INT AUTO_INCREMENT PRIMARY KEY, 
       first_name VARCHAR(50),           
       last_name VARCHAR(50)        
       );

Insert sample records to the savings_accounts table.

mysql> INSERT INTO savings_accounts (first_name, last_name) VALUES ('JOHN', 'DOE');
mysql> INSERT INTO savings_accounts (first_name, last_name) VALUES ('STEVE', 'SMITH');

Query the savings_accounts table to make sure that the records were inserted.

mysql> SELECT
       account_id,
       first_name,
       last_name    
       from savings_accounts;

Ensure that the records are displayed on the MySQL terminal window, as shown below.

+------------+------------+-----------+
| account_id | first_name | last_name |
+------------+------------+-----------+
|          1 | JOHN       | DOE       |
|          2 | STEVE      | SMITH     |
+------------+------------+-----------+
2 rows in set (0.00 sec)

Create a savings_accounts_transactions table. You'll enter the account activities in this table, including credits and debits made by account holders defined in the savings_accounts table.

mysql> CREATE TABLE savings_accounts_transactions (
       ref_id INT AUTO_INCREMENT PRIMARY KEY,
       transaction_type VARCHAR(50), 
       account_id INT,           
       debit DOUBLE(17,2),
       credit DOUBLE(17,2)           
       );

Don't insert any data into the savings_accounts_transactions table, you'll do this in the next step using PHP and MySQL transactions.

Exit from the MySQL console.

quit;

2. Create a PHP File for Handling MySQL Transactions

Open a new file /var/www/html/savings_accounts_transactions.php in the root directory of your server.

$ sudo nano /var/www/html/savings_accounts_transactions.php

Then, insert the information shown below in the file.

<?php

    try {
            $db_name     = 'bank_db';
            $db_user     = 'bank_user';
            $db_password = 'EXAMPLE_PASSWORD';
            $db_host     = 'localhost';

            $pdo = new PDO('mysql:host=' . $db_host . '; dbname=' . $db_name, $db_user, $db_password);
            $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

            $transactions   = [];

            $transactions[] = [
                              'transaction_type' => 'DEBIT - MONEY OUT',
                              'account_id'       => 1,
                              'debit'            => 3000,
                              'credit'           => 0
                              ]; 

            $transactions[] = [
                              'transaction_type' => 'CREDIT - MONEY IN',
                              'account_id'       => 2,
                              'debit'            => 0,
                              'credit'           => 3000
                              ];

            $pdo->beginTransaction();   

            foreach ($transactions as $transaction) {                  
                $sql = 'insert into savings_accounts_transactions
                        (
                        transaction_type,
                        account_id,
                        debit,
                        credit
                        )
                        VALUES
                        (
                        :transaction_type,
                        :account_id,
                        :debit,
                        :credit
                        )
                       ';

                $stmt = $pdo->prepare($sql);
                $stmt->execute($transaction);
               
            }

            $pdo->commit();

            echo "Transactions committed successfully";

        } catch (PDOException $e) {
            $pdo->rollBack();
            echo 'Database error. counter was ' . $i . ' ' . $e->getMessage();
        }  

Save and close the file when you're through with editing.

Explanation of the /var/www/html/savings_accounts_transactions.php file

...
$db_name     = 'bank_db';
$db_user     = 'bank_user';
$db_password = 'EXAMPLE_PASSWORD';
$db_host     = 'localhost';

$pdo = new PDO('mysql:host=' . $db_host . '; dbname=' . $db_name, $db_user, $db_password);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
...

You're connecting the MySQL database that you've created using the code snippet above.

$transactions   = [];

$transactions[] = [
                  'transaction_type' => 'DEBIT - MONEY OUT',
                  'account_id'       => 1,
                  'debit'            => 3000,
                  'credit'           => 0
                  ]; 
$transactions[] = [
                  'transaction_type' => 'CREDIT - MONEY IN',
                  'account_id'       => 2,
                  'debit'            => 0,
                  'credit'           => 3000
                  ];  

In the above code, you've initialized an array of transactions to be executed and placed them in the $transactions variable.

You're debiting $3,000 from a sender's account and crediting it to a receiver's account.

Remember, the two transactions must be executed atomically; otherwise, any attempted change to the database must be rolled back or reversed to maintain database consistency if any of the transactions fail.

$pdo->beginTransaction();   

You've started a transaction by calling the $pdo->beginTransaction() command. You've done this early in the code to ensure that MySQL doesn't commit the changes to the disk unless you issue the $pdo->commit() command.

...
foreach ($transactions as $transaction) {                  
    $sql = 'insert into savings_accounts_transactions
            (
            transaction_type,
            account_id,
            debit,
            credit
            )
            VALUES
            (
            :transaction_type,
            :account_id,
            :debit,
            :credit
            )
            ';

            $stmt = $pdo->prepare($sql);
            $stmt->execute($transaction);
               
}
...

You've used the PHP for...each loop to execute each transaction through a prepared statement by executing the command $pdo->prepare($sql).

You've then executed each transaction using the $stmt->execute($transaction) command. Please note, up to this point, the transactions are not yet committed to disk.

...
$pdo->commit();
echo "Transactions commited succesfully";
...

By executing the code snippet above, you're committing the two transactions to the database permanently and echoing out a success message.

...
try {

} catch (PDOException $e) {
    $pdo->rollBack();
    echo 'Database error. counter was ' . $i . ' ' . $e->getMessage();
}  

You've enclosed everything in the try{...}catch (){...} block; if any error is encountered (e.g. if the server shuts down or the MySQL service stops) when the transactions are being executed, the $pdo->rollBack() command rolls back the changes.

This is where the beauty of MySQL transactions comes in. If any of the transaction fails, the changes are never committed permanently to disk. That is, MySQL transactions have an 'all-or-nothing' effect.

3. Test the PHP Script

Execute the below curl command to test the above PHP script.

$ curl -X GET http://localhost/savings_accounts_transactions.php

After running the above code, you should get a success message as shown below.

Transactions committed successfully

Log back to the MySQL command-line interface to confirm if the records were inserted.

$ sudo mysql -u bank_user -p

Enter the password for the bank_user when prompted and hit Enter to continue.

Select the bank_db database.

mysql> USE bank_db;

Query both the savings_accounts and savings_accounts_transactions table using a MySQL JOIN command and see if the two records were inserted successfully and grab the account holders' names.

mysql> SELECT 
       ref_id,
       concat(savings_accounts.first_name, ' ' , savings_accounts.last_name) as client_name,
       transaction_type,
       savings_accounts_transactions.account_id,
debit,
       credit
       FROM savings_accounts_transactions
       LEFT JOIN savings_accounts
       ON savings_accounts_transactions.account_id = savings_accounts.account_id;

Ensure the output shown below is displayed on the screen.

+--------+-------------+-------------------+------------+---------+---------+
| ref_id | client_name | transaction_type  | account_id | debit   | credit  |
+--------+-------------+-------------------+------------+---------+---------+
|      1 | JOHN DOE    | DEBIT - MONEY OUT |          1 | 3000.00 |    0.00 |
|      2 | STEVE SMITH | CREDIT - MONEY IN |          2 |    0.00 | 3000.00 |
+--------+-------------+-------------------+------------+---------+---------+
2 rows in set (0.00 sec)

Conclusion

In this guide, you've implemented MySQL transactions using PDO (PHP Data Objects) on Ubuntu 20.04. You may tweak this code to suit your needs in applications that require ACID compliance.