Best way to prevent a value from going negative in mysql

前端 未结 3 1333
鱼传尺愫
鱼传尺愫 2021-01-28 19:46

We have a table that maintains account balances by recording transactions in that table. i.e. the most recent row is the account balance.

When recording a withdrawal, we

相关标签:
3条回答
  • 2021-01-28 19:55

    I cannot say anything about the performance of the query, sorry. But you might want to consider triggers to prevent the case of the 'new_balance' ever becoming negative. (Because it strikes me as odd to do a null-insert in case the 'new_balance' is lower than $amount, but it might work nevertheless :) ).

    See documenation of MySQL 5.0 for details how to create a trigger.

    Basically you would put the check, if NEW.new_balance ís negative into a BEFORE-trigger. If yes, then you would use a "STOP ACTION", a deliberate error in execution, to abort the trigger and INSERT-query. See ideas on the mentioned page in the comments.

    Update: Tinkered a little bit around (my excuse for installing MySQL at home).

    My version has the problem of writing a second time to the DB for each value entered into moneylog.

    Maybe switching to a stored proc would be advisable. Or somebody else has a better idea, I'm not that much into DB :)

    CREATE DATABASE triggertest;
    CONNECT triggertest;
    
    CREATE TABLE transferlog (
      account SMALLINT UNSIGNED NOT NULL ,
      amount INT NOT NULL,
      new_balance INT NOT NULL
    ) ENGINE=INNODB;
    
    CREATE TABLE stopaction (
      entry CHAR(20) NOT NULL,
      dummy SMALLINT,
      UNIQUE(`entry`)
    );
    
    INSERT INTO stopaction (`entry`) VALUES ('stop');
    
    DELIMITER #
    CREATE TRIGGER nonneg_insert BEFORE INSERT ON transferlog
      FOR EACH ROW BEGIN
        INSERT INTO stopaction (`entry`)
          SELECT CASE WHEN NEW.new_balance<0 THEN 'stop'
                      ELSE 'none' END;
        DELETE FROM stopaction WHERE entry!='stop';
      END;
    #
    
    CREATE TRIGGER nonneg_update BEFORE UPDATE ON transferlog
      FOR EACH ROW BEGIN
        INSERT INTO stopaction (`entry`)
          SELECT CASE WHEN NEW.new_balance<0 THEN 'stop'
                      ELSE 'none' END;
        DELETE FROM stopaction WHERE entry!='stop';
      END;
    #
    DELIMITER ;
    
    
    INSERT INTO transferlog (`account`, `amount`, `new_balance`)  
      VALUES (1, 1000, 1000);
    INSERT INTO transferlog (`account`, `amount`, `new_balance`)
      VALUES (1, -1000, 0);
    INSERT INTO transferlog (`account`, `amount`, `new_balance`)
      VALUES (1, -1000, -1000);
    INSERT INTO transferlog (`account`, `amount`, `new_balance`)
      VALUES (1, 10, 20);
    SELECT version();
    
    DROP DATABASE triggertest;
    

    Maybe it will suit you, my output for the INSERT-Lines is:

    mysql> INSERT INTO transferlog (`account`, `amount`, `new_balance`)  VALUES (1, 1000, 1000);
    Query OK, 1 row affected (0.03 sec)
    
    mysql> INSERT INTO transferlog (`account`, `amount`, `new_balance`)  VALUES (1, -1000, 0);
    Query OK, 1 row affected (0.02 sec)
    
    mysql> INSERT INTO transferlog (`account`, `amount`, `new_balance`)  VALUES (1, -1000, -1000);
    ERROR 1062 (23000): Duplicate entry 'stop' for key 1
    
    mysql> INSERT INTO transferlog (`account`, `amount`, `new_balance`)  VALUES (1, 10, 20);
    Query OK, 1 row affected (0.02 sec)
    
    mysql> SELECT version();
    +---------------------+
    | version()           |
    +---------------------+
    | 5.0.67-community-nt |
    +---------------------+
    1 row in set (0.00 sec)
    
    0 讨论(0)
  • 2021-01-28 19:55

    Why don't you just do:

    INSERT INTO `txns`
      (`account_id`, `prev_balance`, `txn_type`, `new_balance`,  `amount`, `description`)
    SELECT *
    FROM (
     SELECT
      t.account_id, t.new_balance, $txn_type, t.new_balance - $amount, $amount, $description
     FROM `txns` t
     WHERE t.account_id = '$account'
     ORDER BY txn_id desc
     LIMIT 1
    )
    WHERE new_balance - $amount > 0
    
    0 讨论(0)
  • 2021-01-28 19:58

    I have to agree with the trigger idea. If this is an accounting rule that must be followed no matter how the data is entered, it needs to be in a trigger.

    If this is true only for this particular case, then do it in the SQL code. I don't know mySQL but in SQL server Iwould put the check in an if statment and fail the transaction if the IF condition is met. The critical thing here isn't to ignore the data but to actively fail the transaction,otherwise the user thinks the data has been entered when it has not met the criteria to be entered. I would never write any cdode for a finacial system that isn't encapsulated in transactions which would rollback the entire transaction and send an error to the user if the business rules are not met. Business rules are extremely critical to financial applications (and should usually be in triggers so that they are never missed no matter how the data is put into the system) and data integrity can be a real problem if all the steps do not succeed and you are not in a transaction and rolling back when there is a problem.

    0 讨论(0)
提交回复
热议问题