Search code examples
mysqlstored-procedurestriggersdeadlockautocommit

Mirror tables: triggers, deadlock and implicit commits


I'm have 2 similar tables, for example A and B. I want to replicate insertions in A to B, and insertions in B to A to integrate two users systems . I configured "after insert triggers" on each one. Example:

DELIMITER $$
CREATE DEFINER = `root`@`localhost` TRIGGER
`after_A_INSERT`
AFTER INSERT ON `A`
FOR EACH ROW BEGIN 
INSERT INTO `B`
SET `id` = NEW.`id`,`name` = NEW.`name`;
END$$
DELIMITER ;

DELIMITER $$
CREATE DEFINER = `root`@`localhost` TRIGGER
`after_B_INSERT`
AFTER INSERT ON `B`
FOR EACH ROW BEGIN 
INSERT INTO `A`
SET `id` = NEW.`id`,`name` = NEW.`name`;
END$$
DELIMITER ;

If I insert in A, the triggers calls an insert in B, but this insert executes the trigger in B and a deadlock occurs avoiding a infinite loop.

I've tried to edit triggers to DROP the another table trigger before do the INSERT and then CREATE it again after it. Example:

DELIMITER $$
CREATE DEFINER = `root`@`localhost` TRIGGER
`after_B_INSERT`
AFTER INSERT ON `B`
FOR EACH ROW BEGIN 
DROP TRIGGER IF EXISTS `after_A_INSERT`;
INSERT INTO `A`
SET `id` = NEW.`id`, `name` = NEW.`name`;
/* And CREATE again here */
END$$
DELIMITER ;

However CREATE is a Data Definition Language (DDL) statement that makes an implicit commit. Thus, this can't be done.

I've tried to call a PROCEDURE with the DROP inside to handle explicitly the commit, but isn't possible too.

Any suggestion to mirror this 2 tables?


UPDATE: Using Bill Karwin suggestion, I added a origin field to each table with a respective default vale A or B. Then, I alter (DROP and reCREATE) the triggers as follows:

Trigger in A:

...
BEGIN 
IF NEW.`origin`='A' THEN
    INSERT INTO `B`
        SET `id` = NEW.`id`, `name` = NEW.`name`,  `origin` = NEW.`origin`;
    END IF;
END

Trigger in B:

...
BEGIN 
IF NEW.`origin`='B' THEN
    INSERT INTO `A`
        SET `id` = NEW.`id`, `name` = NEW.`name`, `origin` = NEW.`origin`;
    END IF;
END

Solution

  • You need some way to avoid creating a cycle.

    I'd suggest adding a column origin in both tables. In table A, make the DEFAULT 'A'. In table B, make the DEFAULT 'B'.

    When inserting to either table in your application, always omit the origin column, allowing it to take its default value.

    In both triggers, replicate to the other table only if the NEW.origin is equal to the respective table's default.


    Re your comment and new error:

    Sorry, I forgot to mention that in the trigger when inserting to the other table, you must also copy the value of NEW.origin. Just in your application when you do the original insert do you omit origin.

    Trigger in A:

    ...
    BEGIN 
    IF NEW.`origin`='A' THEN
        INSERT INTO `B`
            SET `id` = NEW.`id`, `name` = NEW.`name`, `origin` = NEW.`origin`;
        END IF;
    END
    

    Trigger in B:

    ...
    BEGIN 
    IF NEW.`origin`='B' THEN
        INSERT INTO `A`
            SET `id` = NEW.`id`, `name` = NEW.`name`, `origin` = NEW.`origin`;
        END IF;
    END
    

    I created these triggers and then tested:

    mysql> insert into A set name = 'bill';
    Query OK, 1 row affected (0.00 sec)
    
    mysql> insert into B set name = 'john';
    Query OK, 1 row affected (0.01 sec)
    
    mysql> select * from A;
    +----+------+--------+
    | id | name | origin |
    +----+------+--------+
    |  1 | bill | A      |
    |  2 | john | B      |
    +----+------+--------+
    2 rows in set (0.00 sec)
    
    mysql> select * from B;
    +----+------+--------+
    | id | name | origin |
    +----+------+--------+
    |  1 | bill | A      |
    |  2 | john | B      |
    +----+------+--------+