shmth
shmth

Reputation: 498

How to conditional update SQL tables?

I have two database tables like so:

CREATE TABLE IF NOT EXISTS accounts (
  id        INT PRIMARY KEY NOT NULL,
  balance   INT NOT NULL DEFAULT 0
);

CREATE TABLE IF NOT EXISTS transactions (
  from      INT FOREIGN KEY REFERENCES accounts(id),
  to        INT FOREIGN KEY REFERENCES accounts(id),
  amount    INT NOT NULL
)

In my application logic, whenever a new transaction request comes in, I want to check first if the from account has sufficient balance, then I want to subtract the amount from the from account, add amount to the to account, and add a new row to the transactions table.

However, I want to do all of this atomically, for two reasons:

Is it possible to do this in a single SQL statement? assume we are using Postgres.

I know for example I can do UPDATE accounts SET balance = balance - 6 WHERE id = 1 and balance > 6; to check if account has sufficient balance and simultaneously update the balance at the same time.

I've also heard of something called select ... for update but I'm not sure if this can help me here.

Also, the result of the query should indicate whether the transaction was successful, so I can display some error message to client if it is not?

Upvotes: 2

Views: 479

Answers (1)

Laurenz Albe
Laurenz Albe

Reputation: 246178

The solution is not to cram everything into a single SQL statement. Apart from being complicated, it will not necessarily protect you from race conditions.

Use database transactions. To avoid anomalies, either lock everything you read with SELECT ... FOR UPDATE against concurrent modifications, or use the REPEATABLE READ Isolation level and repeat the transaction if you get a serialization error.

To protect yourself from deadlocks, make sure that you always lock the account with the lower id first.

Here is a pseudo-code example:

EXEC SQL START TRANSACTION;

if (id1 < id2) {
    EXEC SQL SELECT balance INTO :bal1 FROM accounts
         WHERE id = :id1 FOR UPDATE;
    EXEC SQL SELECT balance INTO :bal2 FROM accounts
         WHERE id = :id2 FOR UPDATE;
} else {
    EXEC SQL SELECT balance INTO :bal2 FROM accounts
         WHERE id = :id2 FOR UPDATE;
    EXEC SQL SELECT balance INTO :bal1 FROM accounts
         WHERE id = :id1 FOR UPDATE;
}

if (bal1 < amount) {
    EXEC SQL ROLLBACK;
    throw_error('too little money on account');
}

EXEC SQL UPDATE accounts SET balance = :bal1 - :amount WHERE id = :id1;
EXEC SQL UPDATE accounts SET balance = :bal1 + :amount WHERE id = :id2;
EXEC SQL INSERT INTO transaction ("from", "to", amount)
    VALUES (:id1, :id2, :amount);

EXEC SQL COMMIT;

Upvotes: 1

Related Questions