需要 mysql 模式设计帮助 - 当前模式需要触发器内的动态 sql

发布于 2024-10-02 07:05:54 字数 3154 浏览 5 评论 0原文

我想我的数据库设计得很糟糕,但我目前对我需要在触发器中使用动态sql这一事实感到困惑,这让mysql不高兴。

背景是我创建了一个包含几十个表的会员数据库,其中主要的一个是具有唯一主键“id”的“member”表。还有许多其他表具有引用 member.id 字段的外键。

由于数据已收集多年且几乎没有重复控制,因此“member”表中还有另一个名为“superseded_by”的字段,其中包含取代该字段的成员的 ID。默认情况下,superseded_by 设置为member_id。任何其 superseded_by <> 的人id 被认为是一个骗子。

现在棘手的部分是……当我们识别出一个骗局时,我们希望将 superseded_by 字段设置为指向新的主要成员并使用指向现在冗余的成员 ID 的外键更新所有表。我尝试使用更新后触发器来做到这一点...然后我尝试通过从 information_schema 查询外键并使用动态 sql 来更新它们来变得聪明。

这显然不起作用(错误代码:1336 存储函数或触发器中不允许动态 SQL)。

我假设有一种更好的方法来设计我没有想到的模式/处理欺骗。

请帮忙...

代码片段:

-- ---
-- Table 'member'
-- ---
DROP TABLE IF EXISTS member;
CREATE TABLE member (
    id INTEGER AUTO_INCREMENT,
    superseded_by INTEGER DEFAULT NULL,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    date_of_birth DATE DEFAULT NULL,
    gender ENUM('M', 'F') DEFAULT NULL,
    mailing_address_id INTEGER DEFAULT NULL,
    last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    FOREIGN KEY (mailing_address_id) REFERENCES mailing_address (id),
    FOREIGN KEY (superseded_by) REFERENCES member (id)
);
DELIMITER $$
    CREATE TRIGGER set_superseded_by_on_insert BEFORE INSERT ON member FOR EACH ROW
    BEGIN
        SET NEW.superseded_by = NEW.id;
    END$$

    -- Trigger to update other tables (volunteers, donations, presenters, etc.) when member's superseded_by record is updated
    -- Assumes the new superseding person exists (they should also not be superseded by anyone themselves)
    CREATE TRIGGER adjust_foreign_member_keys_on_superseded_by_update AFTER UPDATE ON member FOR EACH ROW
    BEGIN
        DECLARE db, tbl, col VARCHAR(64);
        DECLARE fk_update_statement VARCHAR(200);
        DECLARE no_more_rows BOOLEAN;
        DECLARE fks CURSOR FOR  SELECT kcu.TABLE_SCHEMA, kcu.TABLE_NAME, kcu.COLUMN_NAME
                                FROM information_schema.TABLE_CONSTRAINTS tc
                                JOIN information_schema.KEY_COLUMN_USAGE kcu ON
                                        tc.table_schema = kcu.table_schema AND tc.constraint_name = kcu.constraint_name
                                WHERE   tc.constraint_type='FOREIGN KEY' AND
                                        kcu.REFERENCED_TABLE_NAME = 'member' AND
                                        kcu.REFERENCED_COLUMN_NAME = 'id';
        DECLARE CONTINUE HANDLER FOR NOT FOUND SET no_more_rows = TRUE;

        IF NEW.superseded_by <> OLD.superseded_by THEN

            OPEN fks;
            SET no_more_rows = FALSE;
            update_loop: LOOP
                FETCH fks INTO db, tbl, col;
                IF no_more_rows THEN
                    LEAVE update_loop;
                END IF;
                SET @fk_update_statement = CONCAT("UPDATE ", db, ".", tbl, " SET ", col, " = NEW.superseded_by WHERE ", col, " = NEW.id;");
                PREPARE stmt FROM @fk_update_statement;
                EXECUTE stmt;
                DEALLOCATE PREPARE stmt;
            END LOOP;
            CLOSE fks;

        END IF;
    END$$
DELIMITER ;

I imagine that I have designed my database badly, but I'm currently stumped by the fact that I need to use dynamic sql in a trigger and that's making mysql unhappy.

The context is that I have created a membership database with several dozen tables, the main one of which is the 'member' table with a unique primary key 'id'. There are a number of other tables which have foreign keys referring to the member.id field.

Because the data has been gathered over many years and with little dupe-control, there is another field in the 'member' table called 'superseded_by', which contains the id of the member who supersedes this one. By default, superseded_by is set to be the member_id. Any one whose superseded_by <> id is deemed to be a dupe.

Now the tricky part... when we identify a dupe, we want to set the superseded_by field to point to the new primary member and update all the tables with foreign keys pointing to the now redundant member id. I have tried to do this using an after update trigger... and then I've tried to be clever by querying the foreign keys from the information_schema and using dynamic sql to update them.

This clearly doesn't work (Error Code: 1336 Dynamic SQL is not allowed in stored function or trigger).

I'm assuming there is a better way to design the schema / handle dupes which I haven't thought of.

Help please...

CODE SNIPPET:

-- ---
-- Table 'member'
-- ---
DROP TABLE IF EXISTS member;
CREATE TABLE member (
    id INTEGER AUTO_INCREMENT,
    superseded_by INTEGER DEFAULT NULL,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    date_of_birth DATE DEFAULT NULL,
    gender ENUM('M', 'F') DEFAULT NULL,
    mailing_address_id INTEGER DEFAULT NULL,
    last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    FOREIGN KEY (mailing_address_id) REFERENCES mailing_address (id),
    FOREIGN KEY (superseded_by) REFERENCES member (id)
);
DELIMITER $
    CREATE TRIGGER set_superseded_by_on_insert BEFORE INSERT ON member FOR EACH ROW
    BEGIN
        SET NEW.superseded_by = NEW.id;
    END$

    -- Trigger to update other tables (volunteers, donations, presenters, etc.) when member's superseded_by record is updated
    -- Assumes the new superseding person exists (they should also not be superseded by anyone themselves)
    CREATE TRIGGER adjust_foreign_member_keys_on_superseded_by_update AFTER UPDATE ON member FOR EACH ROW
    BEGIN
        DECLARE db, tbl, col VARCHAR(64);
        DECLARE fk_update_statement VARCHAR(200);
        DECLARE no_more_rows BOOLEAN;
        DECLARE fks CURSOR FOR  SELECT kcu.TABLE_SCHEMA, kcu.TABLE_NAME, kcu.COLUMN_NAME
                                FROM information_schema.TABLE_CONSTRAINTS tc
                                JOIN information_schema.KEY_COLUMN_USAGE kcu ON
                                        tc.table_schema = kcu.table_schema AND tc.constraint_name = kcu.constraint_name
                                WHERE   tc.constraint_type='FOREIGN KEY' AND
                                        kcu.REFERENCED_TABLE_NAME = 'member' AND
                                        kcu.REFERENCED_COLUMN_NAME = 'id';
        DECLARE CONTINUE HANDLER FOR NOT FOUND SET no_more_rows = TRUE;

        IF NEW.superseded_by <> OLD.superseded_by THEN

            OPEN fks;
            SET no_more_rows = FALSE;
            update_loop: LOOP
                FETCH fks INTO db, tbl, col;
                IF no_more_rows THEN
                    LEAVE update_loop;
                END IF;
                SET @fk_update_statement = CONCAT("UPDATE ", db, ".", tbl, " SET ", col, " = NEW.superseded_by WHERE ", col, " = NEW.id;");
                PREPARE stmt FROM @fk_update_statement;
                EXECUTE stmt;
                DEALLOCATE PREPARE stmt;
            END LOOP;
            CLOSE fks;

        END IF;
    END$
DELIMITER ;

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

风吹雪碎 2024-10-09 07:05:54

为什么要尝试在主表中维护重复项?似乎您最好使用member 表和member_history 表来跟踪以前的更改。您可以通过一个表来存储更改的字段、更改的日期以及旧值和新值来做到这一点。或者您可以在更新之前存储成员表的先前快照。例如:

INSERT INTO member_history SELECT NULL, * FROM member WHERE id = ?
UPDATE member SET [...] WHERE id = ?

member_history 的架构几乎相同,只是您将member.id 存储为member_id 并且每个历史记录条目都有一个单独的主键。 (注意:我稍微掩盖了语法,NULL, * 部分可能不起作用,在这种情况下,您可能需要显式命名所有字段。还没有花时间检查它)。

CREATE TABLE member (
    id INTEGER AUTO_INCREMENT,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    date_of_birth DATE DEFAULT NULL,
    gender ENUM('M', 'F') DEFAULT NULL,
    mailing_address_id INTEGER DEFAULT NULL,
    last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    FOREIGN KEY (mailing_address_id) REFERENCES mailing_address (id),
);

CREATE TABLE member_history (
    id INTEGER AUTO_INCREMENT,
    member_id INTEGER NOT NULL,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    date_of_birth DATE DEFAULT NULL,
    gender ENUM('M', 'F') DEFAULT NULL,
    mailing_address_id INTEGER DEFAULT NULL,
    last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    FOREIGN KEY (member_id) REFERENCES member (id),
);

请注意,我删除了member 表中的superseded_by 字段以及member_history 表中mailing_address 的外键。您不应该再需要 superseded_by 并且将外键保留在 member_history 表中并不是真正必要的,除非您担心历史记录中存在悬空引用。

Why are you trying to maintain duplicates in your main table? Seems like you'd be better off with a member table and a member_history table to track previous changes. You could do it by having a table that stored the field changed, date changed and the old and new values. Or you could just store the previous snapshot of the member table before updating it. For instance:

INSERT INTO member_history SELECT NULL, * FROM member WHERE id = ?
UPDATE member SET [...] WHERE id = ?

The schema for member_history would be nearly identical except that you would store member.id as member_id and have a separate primary key for each history entry. (Note: I'm glossing over the syntax a little, the NULL, * part might not work in which case you may need to explicitly name all the fields. Haven't taken the time to check it).

CREATE TABLE member (
    id INTEGER AUTO_INCREMENT,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    date_of_birth DATE DEFAULT NULL,
    gender ENUM('M', 'F') DEFAULT NULL,
    mailing_address_id INTEGER DEFAULT NULL,
    last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    FOREIGN KEY (mailing_address_id) REFERENCES mailing_address (id),
);

CREATE TABLE member_history (
    id INTEGER AUTO_INCREMENT,
    member_id INTEGER NOT NULL,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    date_of_birth DATE DEFAULT NULL,
    gender ENUM('M', 'F') DEFAULT NULL,
    mailing_address_id INTEGER DEFAULT NULL,
    last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    FOREIGN KEY (member_id) REFERENCES member (id),
);

Notice that I removed the superseded_by field in the member table and the foreign key to mailing_address in the member_history table. You shouldn't need the superseded_by any more and keeping the foreign key in the member_history table isn't really necessary unless you're worried about dangling references in your history.

白首有我共你 2024-10-09 07:05:54

好的,对此有几点想法:

superseded_by 正在引用同一个表上的 id,并且通常等于后者 - 不是在您能够的情况下不过,要识别一个骗局,在这种情况下,它将指向另一个已经存在的成员的 id

鉴于我们可以安全地假设没有 superseded_by 字段会损害外键约束。

我进一步假设尚未识别的欺骗者的 id 和 superseded_by 字段是相等的。

因此,如果上述所有情况均成立,您可以将其他相关表的外键弯曲为引用 superseded_by 而不是 id。通过这种方式,您可以将对重复项所做的更改级联到其他表,并且仍然具有与以前完全相同的约束。

你认为呢?我错过了什么吗?

请注意,只有当您使用 InnoDB 而不是 MyISAM 时,才可以选择此选项。

问候,
埃夫克斯

Ok, just a couple of thoughts on this:

superseded_by is referencing id on the same table and is in general equal to the latter - not in those cases where you were able to identify a dupe, though, in which case it would point to another already existing member's id.

Given that we can safely assume that no superseded_by field will ever hurt the foreign key constraint.

I further assume that id and superseded_by fields of dupes that have not been identified yet are equal.

So, if all of the above is true, you may bend the foreign key of the other related tables to reference superseded_by instead of id. This way you could cascade the changes made to the dupe down to the other tables and still have the exact same constraint as before.

What you think? Am I missing something?

Please note that this is an option only if you are using InnoDB rather than MyISAM.

Regards,
aefxx

蒲公英的约定 2024-10-09 07:05:54

mysql中的触发器和存储函数都有局限性,我们不能在这两者中使用动态sql。我希望这有帮助。

Trigger and stored function in mysql have limitations that we can not use dynamic sql in both of these. I hope this helps.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文