Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Transactions

Transactions ensure that a group of database operations either all succeed or all fail together.

The safest way to use transactions:

$driver->begin(function($driver) {
    $driver->execute("INSERT INTO orders (user_id, total) VALUES (?, ?)", [1, 100]);
    $driver->execute("UPDATE inventory SET quantity = quantity - 1 WHERE product_id = ?", [42]);

    return true;  // Commit the transaction
});

Automatic Rollback on Exception

If an exception is thrown, the transaction is rolled back:

try {
    $driver->begin(function($driver) {
        $driver->execute("INSERT INTO orders ...", [...]);

        if ($insufficientInventory) {
            throw new \Exception("Not enough inventory");
        }

        $driver->execute("UPDATE inventory ...", [...]);
        return true;
    });
} catch (\Exception $e) {
    // Transaction was automatically rolled back
    echo "Order failed: " . $e->getMessage();
}

Explicit Rollback

Return false or don’t return to rollback:

$driver->begin(function($driver) {
    $driver->execute("INSERT INTO orders ...", [...]);

    if ($someCondition) {
        return false;  // Rollback
    }

    return true;  // Commit
});

Imperative Style

For more control, use imperative transaction methods:

$driver->begin();

try {
    $driver->execute("INSERT INTO orders ...", [...]);
    $driver->execute("UPDATE inventory ...", [...]);

    $driver->commit();
} catch (\Exception $e) {
    $driver->rollback();
    throw $e;
}

Savepoints

Savepoints allow partial rollbacks within a transaction:

$driver->begin(function($driver) {
    $driver->execute("INSERT INTO orders ...", [...]);

    $driver->savepoint('before_notifications');

    try {
        $driver->execute("INSERT INTO notifications ...", [...]);
    } catch (\Exception $e) {
        // Rollback just the notification, keep the order
        $driver->rollbackToSavepoint('before_notifications');
    }

    return true;  // Commit the order
});

Releasing Savepoints

Free savepoint resources when no longer needed:

$driver->begin(function($driver) {
    $driver->savepoint('checkpoint1');

    // ... operations ...

    $driver->releaseSavepoint('checkpoint1');  // Free resources

    return true;
});

Nested Transactions

php-sqlx uses savepoints for nested transactions:

$driver->begin(function($driver) {
    $driver->execute("INSERT INTO users ...", [...]);

    // Inner "transaction" uses a savepoint
    $driver->begin(function($driver) {
        $driver->execute("INSERT INTO profiles ...", [...]);
        return true;
    });

    return true;
});

Transaction Isolation Levels

Set isolation level before starting the transaction:

// PostgreSQL
$driver->execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
$driver->begin(function($driver) {
    // Operations run with SERIALIZABLE isolation
    return true;
});

// MySQL
$driver->execute("SET TRANSACTION ISOLATION LEVEL READ COMMITTED");
$driver->begin(function($driver) {
    return true;
});

Read-Only Transactions

For read-only operations (enables optimizations):

// PostgreSQL
$driver->execute("SET TRANSACTION READ ONLY");
$driver->begin(function($driver) {
    $data = $driver->queryAll("SELECT * FROM reports WHERE ...");
    return true;
});

Connection Pinning

Transactions automatically pin to a single connection. For other scenarios requiring connection affinity (like temporary tables), use withConnection():

$driver->withConnection(function($driver) {
    $driver->execute("CREATE TEMP TABLE temp_data (id INT)");
    $driver->execute("INSERT INTO temp_data VALUES (1), (2), (3)");

    $result = $driver->queryAll("SELECT * FROM temp_data");
    // Temp table exists for this callback's duration

    return $result;
});

Error Handling

TransactionException

Thrown for transaction-specific errors:

use Sqlx\Exceptions\TransactionException;

try {
    $driver->begin(function($driver) {
        // ...
    });
} catch (TransactionException $e) {
    // Transaction failed (deadlock, serialization failure, etc.)
    echo "Transaction error: " . $e->getMessage();
}

Retrying on Transient Failures

Some transaction failures are transient (deadlocks, serialization failures):

$maxRetries = 3;
$attempt = 0;

while ($attempt < $maxRetries) {
    try {
        $driver->begin(function($driver) {
            // ... operations ...
            return true;
        });
        break;  // Success
    } catch (TransactionException $e) {
        if ($e->isTransient() && $attempt < $maxRetries - 1) {
            $attempt++;
            usleep(100000 * $attempt);  // Exponential backoff
            continue;
        }
        throw $e;
    }
}

Best Practices

Keep Transactions Short

// GOOD - Minimal time in transaction
$data = prepareData();  // Outside transaction
$driver->begin(function($driver) use ($data) {
    $driver->execute("INSERT ...", $data);
    return true;
});

// BAD - Long-running operations in transaction
$driver->begin(function($driver) {
    $data = fetchFromExternalApi();  // Slow!
    $driver->execute("INSERT ...", $data);
    return true;
});

Avoid User Interaction

Never wait for user input inside a transaction:

// BAD - Holds transaction open while waiting
$driver->begin(function($driver) {
    $driver->execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
    $confirm = readline("Confirm? ");  // Don't do this!
    if ($confirm === 'yes') {
        return true;
    }
    return false;
});

Handle Deadlocks

Implement retry logic for concurrent write scenarios:

function transferMoney(int $from, int $to, float $amount): void
{
    $maxRetries = 3;

    for ($i = 0; $i < $maxRetries; $i++) {
        try {
            $this->driver->begin(function($driver) use ($from, $to, $amount) {
                $driver->execute(
                    "UPDATE accounts SET balance = balance - ? WHERE id = ?",
                    [$amount, $from]
                );
                $driver->execute(
                    "UPDATE accounts SET balance = balance + ? WHERE id = ?",
                    [$amount, $to]
                );
                return true;
            });
            return;
        } catch (TransactionException $e) {
            if (!$e->isTransient() || $i === $maxRetries - 1) {
                throw $e;
            }
        }
    }
}