Nested Sets + MySQL TRIGGER

    Task


    The task is the same as in the previous article , only applicable to MySQL.

    Rake


    Good news guys! There is no problem with recursive triggers in MySQL ! MySQL developers just stupidly lock a mutable table even at the trigger level, here are the radishes. But, in fact, only a power outage can stop us.
    There is a small loophole, with ... joined tables. Although I did not find confirmation in the documentation that it was so specially conceived, there was no denying either. True, there is a possibility that this loophole can be covered, although I do not see the point.
    Alas, the trigger mechanism in MySQL is new and rather crude, which imposes some restrictions on its use, but still it is enough to solve our problem.
    So, the source table:
    SQL code (1)
    CREATE TABLE `ns_tree` (
        `id` int (11) NOT NULL auto_increment,
        `left_key` int (11) NOT NULL default '0',
        `right_key` int (11) NOT NULL default '0',
        `level` int (11) NOT NULL default '0',
        `parent_id` int (11) NOT NULL default '0',
        `tree` int (11) NOT NULL default '1',
        `field1` text,
        PRIMARY KEY (`id`)
    ) ENGINE = MyISAM;
        

    Based on it, we make exactly the same table, with exactly the same set of fields:
    SQL code (2)
    CREATE TABLE `_ns_tree` (
        `id` int (11) NOT NULL auto_increment,
        `left_key` int (11) NOT NULL default '0',
        `right_key` int (11) NOT NULL default '0',
        `level` int (11) NOT NULL default '0',
        `parent_id` int (11) NOT NULL default '0',
        `tree` int (11) NOT NULL default '1',
        `field1` text,
        PRIMARY KEY (`id`)
    ) ENGINE = MERGE INSERT_METHOD = LAST UNION = (`ns_tree`);
        

    Keyword - MERGE essentially we create a view of our table, with which we can work as a table. In general, the MERGE mechanism is also a bit damp. The structure of the joined table must be exactly the same as the original.
    IMPORTANT!!! When you change the structure of the original table, the connection between the tables is lost, but between the already connected rows - NO! Be careful. When you change the structure of the source table, you must apply the same change to the combined one!
    Also, the source table must be MyISAM, it is understandable, transactions in this case are not applicable due to the fact that the tables can not lock each other, and the data in them is the same. However, this is not scary for us, since the source table will be locked within the trigger, and we will change the data from the combined table from the triggers, so there can be no intersections of queries.
    Also, what I wanted to draw attention to: I strongly recommend that you do not use root nodes within the same tree (as I mentioned in the previous article), since the table is completely blocked, and too long a queue can be formed.

    Create Record


    The MySQL trigger dialect is slightly different from the PostgreSQL dialect :
    • It is not necessary to declare variables at the beginning of the procedure;
    • It is impossible to interrupt the execution of a trigger, it must work out completely;
    • As mentioned above, we are not afraid of recursion, therefore we will not need extra checks and additional fields;
    Therefore, the logic changes somewhat:
    • Variables appear in the course of the code;
    • Instead of returning from the trigger, we wrap the code with conditions;
    • All changes concerning the structure of the tree are applied to the auxiliary joint table;
    SQL code (3)
    CREATE DEFINER = 'user' @ 'localhost' TRIGGER `ns_tree_before_ins_tr` BEFORE INSERT ON` ns_tree`
        FOR EACH ROW
    BEGIN
        SET @left_key: = 0;
        SET @level: = 0;
    - If we indicated the parent:
        IF NEW.parent_id IS NOT NULL AND NEW.parent_id> 0 THEN
            SELECT right_key, `level` + 1 INTO @left_key, @level
                FROM ns_tree
                WHERE id = NEW.parent_id AND tree = NEW.tree;
        END IF;
    - If we specified the left key:
        IF NEW.left_key IS NOT NULL AND NEW.left_key> 0 AND 
            (@left_key IS NULL OR @left_key = 0) THEN
            SELECT id, left_key, right_key, `level`, parent_id 
                INTO @tmp_id, @tmp_left_key, @tmp_right_key, @tmp_level, @tmp_parent_id
                FROM ns_tree
                WHERE tree = NEW.tree AND (left_key = NEW.left_key OR right_key = NEW.left_key);
            IF @tmp_left_key IS NOT NULL AND @tmp_left_key> 0 AND NEW.left_key = @tmp_left_key THEN
                SET NEW.parent_id: = @tmp_parent_id;
                SET @left_key: = NEW.left_key;
                SET @level: = @tmp_level;
            ELSEIF @tmp_left_key IS NOT NULL AND @tmp_left_key> 0 AND NEW.left_key = @tmp_right_key THEN
                SET NEW.parent_id: = @tmp_id;
                SET @left_key: = NEW.left_key;
                SET @level: = @tmp_level + 1;
            END IF;
        END IF;
    - If the parent or left key is not specified, or we did not find anything
        IF @left_key IS NULL OR @left_key = 0 THEN
            SELECT MAX (right_key) + 1 INTO @left_key
                FROM ns_tree
                WHERE tree = NEW.tree;
            IF @left_key IS NULL OR @left_key = 0 THEN
                SET @left_key: = 1;
            END IF;
            SET @level: = 0;
            SET NEW.parent_id: = 0; 
        END IF;
    - Set new key values
        SET NEW.left_key: = @left_key;
        SET NEW.right_key: = @left_key + 1;
        SET NEW.`level`: = @level;
    - Form a gap in the tree
        UPDATE _ns_tree
            SET left_key = CASE WHEN left_key> = @left_key 
                  THEN left_key + 2 
                  ELSE left_key + 0 
                END,
                right_key = right_key + 2
            WHERE tree = NEW.tree AND right_key> = @left_key;
    END
        


    Edit Record


    The principle is the same with the use of the dialect.
    SQL code (4)
    CREATE DEFINER = 'user' @ 'localhost' TRIGGER `ns_tree_before_upd_tr` BEFORE UPDATE ON` ns_tree`
        FOR EACH ROW
    BEGIN
    - We prohibit changing fields, or sending nasty things
        SET NEW.tree: = OLD.tree;
        SET NEW.right_key: = OLD.right_key;
        SET NEW.`level`: = OLD.`level`;
        SET @return_flag: = 0;
        IF NEW.parent_id IS NULL THEN SET NEW.parent_id: = 0; END IF;
    - Check if there are changes related to the structure of the tree
        IF NEW.parent_id <> OLD.parent_id OR NEW.left_key <> OLD.left_key THEN
    - We are rebuilding the tree, well, let's proceed:
            SET @left_key: = 0;
            SET @level: = 0;
            SET @skew_tree: = OLD.right_key - OLD.left_key + 1;
    - Determine where we are moving it:
    - If parent_id is changed:
            IF NEW.parent_id <> OLD.parent_id THEN
    - If in subjection to another evil:
                IF NEW.parent_id> 0 THEN
                    SELECT right_key, level + 1
                        INTO @left_key, @level
                        FROM ns_tree
                        WHERE id = NEW.parent_id AND tree = NEW.tree;
    - Otherwise, we transfer to the root of the tree:
                ELSE
                    SELECT MAX (right_key) + 1 
                        INTO @left_key
                        FROM ns_tree
                        WHERE tree = NEW.tree;
                    SET @level: = 0;
                END IF;
    - If suddenly the parent is in the range of the moved node, check:
                IF @left_key IS NOT NULL AND 
                   @left_key> 0 AND
                   @left_key> OLD.left_key AND
                   @left_key <= OLD.right_key THEN
                       SET NEW.parent_id: = OLD.parent_id;
                       SET NEW.left_key: = OLD.left_key;
                       SET @return_flag: = 1;
                END IF;
            END IF;
    - If not parent_id, then left_key is changed, or if changing parent_id yields nothing
            IF @left_key IS NULL OR @left_key = 0 THEN
                SELECT id, left_key, right_key, `level`, parent_id 
                    INTO @tmp_id, @tmp_left_key, @tmp_right_key, @tmp_level, @tmp_parent_id
                    FROM ns_tree
                    WHERE tree = NEW.tree AND (right_key = NEW.left_key OR right_key = NEW.left_key - 1)
                    LIMIT 1;
                IF @tmp_left_key IS NOT NULL AND 
                   @tmp_left_key> 0 AND 
                   NEW.left_key - 1 = @tmp_right_key THEN
                    SET NEW.parent_id: = @tmp_parent_id;
                    SET @left_key: = NEW.left_key;
                    SET @level: = @tmp_level;
                ELSEIF @tmp_left_key IS NOT NULL AND 
                       @tmp_left_key> 0 AND 
                       NEW.left_key = @tmp_right_key THEN
                    SET NEW.parent_id: = @tmp_id;
                    SET @left_key: = NEW.left_key;
                    SET @level: = @tmp_level + 1;
                ELSEIF NEW.left_key = 1 THEN
                    SET NEW.parent_id: = 0;
                    SET @left_key: = NEW.left_key;
                    SET @level: = 0;
                ELSE
                    SET NEW.parent_id: = OLD.parent_id;
                    SET NEW.left_key: = OLD.left_key;
                    SET @return_flag = 1;
                END IF;
            END IF;
    - Now we know where we are moving the tree
    - We check whether it is worth doing
            IF @return_flag IS NULL OR @return_flag = 0 THEN
                SET @skew_level: = @level - OLD.`level`;
                IF @left_key> OLD.left_key THEN
    - Move up the tree
                    SET @skew_edit: = @left_key - OLD.left_key - @skew_tree;
                    UPDATE _ns_tree
                        SET left_key = CASE WHEN right_key <= OLD.right_key
                                             THEN left_key + @skew_edit
                                             ELSE CASE WHEN left_key> OLD.right_key
                                                       THEN left_key - @skew_tree
                                                       ELSE left_key
                                                  End
                                       END,
                            `level` = CASE WHEN right_key <= OLD.right_key 
                                            THEN `level` + @skew_level
                                            ELSE `level`
                                       END,
                            right_key = CASE WHEN right_key <= OLD.right_key 
                                             THEN right_key + @skew_edit
                                             ELSE CASE WHEN right_key <@left_key
                                                       THEN right_key - @skew_tree
                                                       ELSE right_key
                                                  End
                                        End
                        WHERE tree = OLD.tree AND
                              right_key> OLD.left_key AND
                              left_key <@left_key AND
                              id <> OLD.id;
                    SET @left_key: = @left_key - @skew_tree;
                ELSE
    - Moving down the tree:
                    SET @skew_edit: = @left_key - OLD.left_key;
                    UPDATE _ns_tree 
                        SET
                            right_key = CASE WHEN left_key> = OLD.left_key
                                             THEN right_key + @skew_edit
                                             ELSE CASE WHEN right_key <OLD.left_key
                                                       THEN right_key + @skew_tree
                                                       ELSE right_key
                                                  End
                                        END,
                            `level` = CASE WHEN left_key> = OLD.left_key
                                             THEN `level` + @skew_level
                                             ELSE `level`
                                        END,
                            left_key = CASE WHEN left_key> = OLD.left_key
                                             THEN left_key + @skew_edit
                                             ELSE CASE WHEN left_key> = @left_key
                                                       THEN left_key + @skew_tree
                                                       ELSE left_key
                                                  End
                                        End
                        WHERE tree = OLD.tree AND
                              right_key> = @left_key AND
                              left_key <OLD.right_key AND
                              id <> OLD.id;
                END IF;
    - The tree was rebuilt, only our current node remained
                SET NEW.left_key: = @left_key;
                SET NEW.`level`: = @level;
                SET NEW.right_key: = @left_key + @skew_tree - 1;
            END IF;
        END IF;
    END
        

    ATTENTION!!! In MySQL, the order in which the fields are listed in the UPDATE query matters , since the fields that are changed during the query in the same query change the value to a new one, so if we use these fields further in the query under conditions, the result will be inadequate.

    Delete record


    Due to the lack of problems with trigger recursion, deleting generally becomes a trivial task.
    Trigger for option: “delete the entire branch”:
    SQL code (5)
    CREATE DEFINER = 'user' @ 'localhost' TRIGGER `ns_tree_before_del_tr` AFTER DELETE ON` ns_tree`
        FOR EACH ROW
    BEGIN
    - Delete the child nodes:
        DELETE FROM _ns_tree
            WHERE 
                tree = OLD.tree AND
                left_key> OLD.left_key AND
                right_key <OLD.right_key;
    - We remove the gap in the keys:
        SET @skew_tree: = OLD.right_key - OLD.left_key + 1;
        UPDATE _ns_tree
            SET left_key = CASE WHEN left_key> OLD.left_key
                                THEN left_key - @skew_tree
                                ELSE left_key
                           END,
                right_key = right_key - @skew_tree
            WHERE right_key> OLD.right_key AND
                tree = OLD.tree AND
                id <> OLD.id;
    END
        

    Trigger for option: “deleting a node with moving child nodes upward”:
    SQL code (6)
    CREATE DEFINER = 'user' @ 'localhost' TRIGGER `ns_tree_before_del_tr` AFTER DELETE ON` ns_tree`
        FOR EACH ROW
    BEGIN
    - We remove the gap in the keys:
       UPDATE _ns_tree
            SET left_key = CASE WHEN left_key <OLD.left_key
                                THEN left_key
                                ELSE CASE WHEN right_key <OLD.right_key
                                          THEN left_key - 1 
                                          ELSE left_key - 2
                                     End
                           END,
                parent_id = CASE WHEN right_key <OLD.right_key AND `level` = OLD.level + 1
                               THEN OLD.parent_id
                               ELSE parent_id
                            END,
                `level` = CASE WHEN right_key <OLD.right_key
                               THEN `level` - 1 
                               ELSE `level`
                          END,
                right_key = CASE WHEN right_key <OLD.right_key
                                 THEN right_key - 1 
                                 ELSE right_key - 2
                            End
            WHERE (right_key> OLD.right_key OR
                (left_key> OLD.left_key AND right_key <OLD.right_key)) AND
                tree = OLD.tree;
    END
        

    I want to draw attention to the fact that changes affecting the structure of the tree need not be made in batches, but sequentially for each node, so that its integrity is maintained.
    Actually everything. It remains only to put down the indexes (again, I am lazy to write SQL commands here, so I’ll just voice them):
    • Composite is not unique to fields ( left_key, right_key, level, tree );
    • Not unique on the field ( parent_id );

    Enjoy ;-)
    Sergey Tomulevich aka Phoinix (07/08/2009)

    Also popular now: