Search code examples
mysqlstored-procedurestransactions

MySQL - Transactional Bound with multiple stored proc calls and easy way to rollback?


first time poster

So my issue/question is this. I have a stored proc that is doing some "stuff". Within this "stuff", I am calling other stored procs as well that are doing "additional stuff".

I want to be able to set the auto commit to OFF in the main SP.

SET SESSION autocommit = 0;

Question is, when the other SPs get called, will it also still obey the autocommit=0? If not and I have to set it manully, how do I ensure the rollback works?

Example:


CREATE PROCEDURE sp_whatever ()

BEGIN
    SET SESSION autocommit = 0;
    DECLARE track_no INT DEFAULT 0;
     DECLARE EXIT HANDLER FOR SQLEXCEPTION, NOT FOUND, SQLWARNING;
    -- Error Handling Block
        errorHandling:BEGIN
            
                ROLLBACK;
                GET DIAGNOSTICS CONDITION 1 @`errno` = MYSQL_ERRNO, @`sqlstate` = RETURNED_SQLSTATE, @`text` = MESSAGE_TEXT;
                SET @full_error = CONCAT('ERROR ', @`errno`, ' (', @`sqlstate`, '): ', @`text`);
                SELECT track_no, @full_error;
            
            END errorHandling;
                
START TRANSACTION;
     DO SOME SELECTS;
     DO SOME DMLS;
     Call some stored_proc();   
     DO SOME MORE SELECTS;
     DO SOME MORE DMLS;
     Call some additional stored_proc_additional();

  COMMIT;       
        
END;

If an error occurs within this sp_whatever proc or in the stored_proc() or stored_proc_additional(), will it rollback the whole transaction or just what is in the local procedure where the error occured. What is best practice to either make it so I can rollback the whole transaction if I am calling other procs when an error occurs? Is this even possible?

If I collapse all my SP into one massive SP, then the issue becomes within each BEGIN...END Block, I am unable to use a PREPARE STATEMENT due to the MySQL docs mentioning:

Because local variables are in scope only during stored program execution, references to them are not permitted in prepared statements created within a stored program. Prepared statement scope is the current session, not the stored program, so the statement could be executed after the program ends, at which point the variables would no longer be in scope. For example, SELECT ... INTO local_var cannot be used as a prepared statement.

Any advice/Thoughts?

Calling multiple Stored Procs but unable to rollback the whole transaction


Solution

  • If you define a catch-all-error exit handler to rollback in the main procedure, it works on all procedures within it when they send an error message.Take this case below for example. We have a procedure n1 which inserts numeric values 1 to 5 into our test table's PK column. Then we have procedure n2 which inserts 6 to 10, but it then inserts number 5 to the table, which raises a duplicate PK value error. Next, in our main SP nn we put those beforementioned procedures under the START TRANSACTION section and add some other stuff besides them.

    create table test (id int primary key);
    delimiter //
    drop procedure if exists n1 //
    drop procedure if exists n2 //
    drop procedure if exists nn //
    
    create procedure n1()
    begin
    declare n int default 1;
    while n<=5 do
    insert test values(n);
    set n=n+1;
    end while;
    
    end//
    
    create procedure n2()
    begin
    declare n int default 6;
    while n<=10 do
    insert test values(n);
    set n=n+1;
    end while;
    insert test values(5); -- this raises a dup pk value error
    
    end//
    
    
    create procedure nn()
    begin
    declare exit handler for sqlexception 
    begin  
    rollback; 
    resignal;
    end;
    
    truncate test;
    
    start transaction ;
    insert test values(100);
    call n1;
    insert test values(200);
    call n2;
    insert test values(300);
    commit;
    end//
    
    delimiter ;
    
    

    Let's call the main procedure:

    call nn;
    ERROR 1062 (23000): Duplicate entry '5' for key 'PRIMARY'
    
    select * from test;
    Empty set (0.00 sec)
    

    As the result shows, the whole session is rolled back.

    To wrap up, within your main procedure, if you have predefined an exit handler to rollback when an error is found, it applies to all the nested procedures under a transaction when they raise an err message , which can be regarded as an ALL-OR-NOTHING job.

    But, this is not the end of it. What if we mute the error notification from a sub SP ? For example, we declare an exit handler in n2 but does not define the RESIGNAL statement, which will make n2 sends a query ok notice to the outside instead of unmasking the true event. Check this out:

    delimiter //
    drop procedure if exists n1 //
    drop procedure if exists n2 //
    drop procedure if exists nn //
    
    create procedure n1()
    begin
    declare n int default 1;
    while n<=5 do
    insert test values(n);
    set n=n+1;
    end while;
    
    end//
    
    create procedure n2()
    begin
    declare n int default 6;
    declare exit handler for sqlexception begin end;
    
    while n<=10 do
    insert test values(n);
    set n=n+1;
    end while;
    insert test values(5); -- this raises a dup pk value error
    
    end//
    
    
    create procedure nn()
    begin
    declare exit handler for sqlexception 
    begin  
    rollback; 
    resignal;
    end;
    
    truncate test;
    
    start transaction ;
    insert test values(100);
    call n1;
    insert test values(200);
    call n2;
    insert test values(300);
    commit;
    end//
    
    delimiter ;
    
    call nn;
    Query OK, 0 rows affected (0.02 sec)
    
     select * from test;
    
    +-----+
    | id  |
    +-----+
    |   1 |
    |   2 |
    |   3 |
    |   4 |
    |   5 |
    |   6 |
    |   7 |
    |   8 |
    |   9 |
    |  10 |
    | 100 |
    | 200 |
    | 300 |
    +-----+
    
    

    As we can see, since the error message is no longer received in the main SP nn, its exit handler will no longer be triggered, thus no rollback is performed.

    In other words , you can tweak the sub SP to make it work to your favour like muting error message from certain SP which we just did. You can even customise error code in different SP and declare specific handlers in the main SP to address each code. It's for you to decide.

    But beware, a commit or rollback from a nested SP also applies to the outside. In the simplified example below (no condition hanlder declared this time) , n2 has a rollback at the end which not only works on itself but spreads outside as well.

    delimiter //
    drop procedure if exists n1 //
    drop procedure if exists n2 //
    drop procedure if exists nn //
    
    create procedure n1()
    begin
    declare n int default 1;
    
    while n<=5 do
    insert test values(n);
    set n=n+1;
    end while;
    
    end//
    
    create procedure n2()
    begin
    declare n int default 6;
    
    while n<=10 do
    insert test values(n);
    set n=n+1;
    end while;
    
    rollback;
    
    end//
    
    
    create procedure nn()
    begin
    
    truncate test;
    
    start transaction ;
    insert test values(100);
    call n1;
    insert test values(200);
    call n2; -- there is a rollback at the end of the procedure
    insert test values(300);
    commit;
    end//
    
    delimiter ;
    
     call nn;
    Query OK, 0 rows affected (0.06 sec)
    
    select * from test;
    +-----+
    | id  |
    +-----+
    | 300 |
    +-----+
    
    

    As denoted above, the rollback from n2 undoes all changes in the transaction so far.

    Finally, please be care when setting autocommit off. As its effect lasts as long as the session stays until toggled on. It may potentially give rise to loss of data when the mind intends the behaviors of autocommit. Use of START TRANSACTION is safer in this regard.

    UPDATED with procedure n2.1 thrown in n2

    delimiter //
    drop procedure if exists n1 //
    drop procedure if exists n2 //
    drop procedure if exists `n2.1` //
    drop procedure if exists nn //
    
    create procedure n1()
    begin
    declare n int default 1;
    
    while n<=5 do
    insert test values(n);
    set n=n+1;
    end while;
    
    end//
    
    create procedure `n2.1`()
    begin
    insert test values(999);
    end//
    
    create procedure n2()
    begin
    declare n int default 6;
    
    call `n2.1`;
    while n<=10 do
    insert test values(n);
    set n=n+1;
    end while;
    
    end//
    
    
    create procedure nn()
    begin
    
    truncate test;
    
    start transaction ;
    insert test values(100);
    call n1;
    insert test values(200);
    call n2; 
    insert test values(300);
    commit;
    end//
    
    delimiter ;
    
    

    Let's call it:

    
    call nn;
    
    select * from test;
    +-----+
    | id  |
    +-----+
    |   1 |
    |   2 |
    |   3 |
    |   4 |
    |   5 |
    |   6 |
    |   7 |
    |   8 |
    |   9 |
    |  10 |
    | 100 |
    | 200 |
    | 300 |
    | 999 |
    +-----+
    
    

    In addition, to demonstrate if a SQLEXCEPTION from 3 levels deep can be handled by outside handler, I'm purposely making n2.1 blunder. Then let's see if the exit handler in our main nn will deal with it.

    delimiter //
    drop procedure if exists n1 //
    drop procedure if exists n2 //
    drop procedure if exists `n2.1` //
    drop procedure if exists nn //
    
    create procedure n1()
    begin
    declare n int default 1;
    
    while n<=5 do
    insert test values(n);
    set n=n+1;
    end while;
    
    end//
    
    create procedure `n2.1`()
    begin
    insert test values(999);
    insert test values(999);
    end//
    
    create procedure n2()
    begin
    declare n int default 6;
    
    call `n2.1`;
    while n<=10 do
    insert test values(n);
    set n=n+1;
    end while;
    
    end//
    
    
    create procedure nn()
    begin
    declare exit handler for sqlexception 
    begin  
    commit; 
    resignal;
    end;
    truncate test;
    
    start transaction ;
    insert test values(100);
    call n1;
    insert test values(200);
    call n2; 
    insert test values(300);
    commit;
    end//
    
    delimiter ;
    
    

    Now the moment of truth.

    call nn;
    ERROR 1062 (23000): Duplicate entry '999' for key 'PRIMARY'
    select * from test;
    +-----+
    | id  |
    +-----+
    |   1 |
    |   2 |
    |   3 |
    |   4 |
    |   5 |
    | 100 |
    | 200 |
    | 999 |
    +-----+
    
    

    As the result depicts, it turns out the exit handler in nn took care of the error from n2.1 by committing changes so far and signaling the error message before terminating the procedure.

    And now, in the event that you wonder what happens if both n2 and nn have handlers. I've also prepared two more cases for you. In case 1, n2 mutes the error by removing the resignal in its handler, while in case 2, n2 announces the error with the resignal in its handler. Note, in both cases, n2 uses continue handler this time .

    delimiter //
    drop procedure if exists n1 //
    drop procedure if exists n2 //
    drop procedure if exists `n2.1` //
    drop procedure if exists nn //
    
    create procedure n1()
    begin
    declare n int default 1;
    
    while n<=5 do
    insert test values(n);
    set n=n+1;
    end while;
    
    end//
    
    create procedure `n2.1`()
    begin
    insert test values(999);
    insert test values(999);
    end//
    
    create procedure n2()
    begin
    declare n int default 6;
    declare continue handler for sqlexception 
    begin  
    rollback; 
    end;
    
    call `n2.1`;
    while n<=10 do
    insert test values(n);
    set n=n+1;
    end while;
    
    end//
    
    
    create procedure nn()
    begin
    declare exit handler for sqlexception 
    begin  
    commit; 
    resignal;
    end;
    truncate test;
    
    start transaction ;
    insert test values(100);
    call n1;
    insert test values(200);
    call n2; 
    insert test values(300);
    commit;
    end//
    
    delimiter ;
    
    

    Here is the result for case 1:

    call nn;
    Query OK, 0 rows affected (0.03 sec)
    
    select * from test;
    +-----+
    | id  |
    +-----+
    |   6 |
    |   7 |
    |   8 |
    |   9 |
    |  10 |
    | 300 |
    +-----+
    
    

    As the result reflects, the n2 rolled back changes so far and moved on (withouting raising an error due to the absence of resignal). And nn just carried on as if nothing abnormal had happened so far.

    Now if you are still with me, here comes case 2. Pay attention to the contents of handlers as n2 has commit and resignal , while nn has rollback (and resignal for the sake of notification).

    delimiter //
    drop procedure if exists n1 //
    drop procedure if exists n2 //
    drop procedure if exists `n2.1` //
    drop procedure if exists nn //
    
    create procedure n1()
    begin
    declare n int default 1;
    
    while n<=5 do
    insert test values(n);
    set n=n+1;
    end while;
    
    end//
    
    create procedure `n2.1`()
    begin
    insert test values(999);
    insert test values(999);
    end//
    
    create procedure n2()
    begin
    declare n int default 6;
    declare continue handler for sqlexception 
    begin  
    commit; 
    resignal;
    end;
    
    call `n2.1`;
    while n<=10 do
    insert test values(n);
    set n=n+1;
    end while;
    
    end//
    
    
    create procedure nn()
    begin
    declare exit handler for sqlexception 
    begin  
    rollback; 
    resignal;
    end;
    truncate test;
    
    start transaction ;
    insert test values(100);
    call n1;
    insert test values(200);
    call n2; 
    insert test values(300);
    commit;
    end//
    
    delimiter ;
    
    

    Now we are getting the truth in case 2.

    call nn;
    ERROR 1062 (23000): Duplicate entry '999' for key 'PRIMARY'
    
    select * from test;
    +-----+
    | id  |
    +-----+
    |   1 |
    |   2 |
    |   3 |
    |   4 |
    |   5 |
    | 100 |
    | 200 |
    | 999 |
    +-----+
    
    

    So n2 committed the changes up until now as well as cried an error. HERE IS A BOOKMARK WHICH WE WILL COME BACK SHORTLY AFTER. Upon receiving the error, nn performed a rollback, which of course could not undo the changes already commited by n2, and called off the rest.

    Now let's get back to the bookmark. What do you think really happened when n2 shouted the error? Because for its continue handler, it should carry on its job due to the handler action being continue. But the handler from nn would just rollback things and kill the current procedure. Check the code below to find out. Note there is a commit at the end of n2 so as to make a proof of things truly having happened.

     
    delimiter //
    drop procedure if exists n1 //
    drop procedure if exists n2 //
    drop procedure if exists `n2.1` //
    drop procedure if exists nn //
    
    create procedure n1()
    begin
    declare n int default 1;
    
    while n<=5 do
    insert test values(n);
    set n=n+1;
    end while;
    
    end//
    
    create procedure `n2.1`()
    begin
    insert test values(999);
    insert test values(999);
    end//
    
    create procedure n2()
    begin
    declare n int default 6;
    declare continue handler for sqlexception 
    begin  
    commit; 
    resignal;
    end;
    
    call `n2.1`;
    while n<=10 do
    insert test values(n);
    set n=n+1;
    end while;
    commit;
    end//
    
    
    create procedure nn()
    begin
    declare exit handler for sqlexception 
    begin  
    rollback; 
    resignal;
    end;
    truncate test;
    
    start transaction ;
    insert test values(100);
    call n1;
    insert test values(200);
    call n2; 
    insert test values(300);
    commit;
    end//
    
    delimiter ;
    
    

    Lo and behold, here the curtain unveils.

    call nn;
    ERROR 1062 (23000): Duplicate entry '999' for key 'PRIMARY'
    
    select * from test;
    +-----+
    | id  |
    +-----+
    |   1 |
    |   2 |
    |   3 |
    |   4 |
    |   5 |
    | 100 |
    | 200 |
    | 999 |
    +-----+
    
    

    It ends up n2 did not get to finish the rest of its procedure with the handler action continue. Which means, the exit handler in main procedure nn repealed everything as soon as it received the error from n2 but not before executing its rollback and resignal duty.

    It's a long read which I pray did not bore you too much. Hopefully it helps.