Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MySQL ON UPDATE CASCADE not CASCADEing

Suppose i have two below table:

CREATE TABLE post (
  id bigint(20)     NOT NULL    AUTO_INCREMENT,
  text text ,

  PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=1;

CREATE TABLE post_path (
  ancestorid bigint(20)     NOT NULL    DEFAULT '0',
  descendantid bigint(20)   NOT NULL    DEFAULT '0',
  length int(11)            NOT NULL    DEFAULT '0',

  PRIMARY KEY (ancestorid,descendantid),
  KEY descendantid (descendantid),

  CONSTRAINT f_post_path_ibfk_1 
    FOREIGN KEY (ancestorid) REFERENCES post (id) 
    ON DELETE CASCADE 
    ON UPDATE CASCADE,
  CONSTRAINT f_post_path_ibfk_2 
    FOREIGN KEY (descendantid) REFERENCES post (id) 
    ON DELETE CASCADE 
    ON UPDATE CASCADE
) ENGINE=InnoDB;

And inserted these rows:

INSERT INTO 
    post (text)
    VALUES ('a'); #// inserted row by id=1
INSERT INTO 
    post_path (ancestorid ,descendantid ,length) 
    VALUES (1, 1, 0);

When i want to update post row id:

 UPDATE post SET id = '10' WHERE post.id =1

MySQL said:

#1452 - Cannot add or update a child row: a foreign key constraint fails (test.post_path, CONSTRAINT f_post_path_ibfk_2 FOREIGN KEY (descendantid) REFERENCES post (id) ON DELETE CASCADE ON UPDATE CASCADE) 

Why? what is wrong?

Edit:

When i inserted these rows:

INSERT INTO 
    post (text)
    VALUES ('b'); #// inserted row by id=2

INSERT INTO 
    post_path (ancestorid, descendantid, length)
    VALUES (1, 2, 0);

And updated:

UPDATE post SET id = '20' WHERE post.id =2

Mysql updated successfully both child and parent row. so Why i can not update first post (id=1)?

like image 885
ahoo Avatar asked Apr 12 '13 17:04

ahoo


2 Answers

Ok, I ran your schema and queries through a test database I have access too and noticed the following; after inserting both rows to both tables, and before any updates the data looks like:

mysql> select * from post;
+----+------+
| id | text |
+----+------+
|  1 | a    |
|  2 | b    |
+----+------+
2 rows in set (0.00 sec)

mysql> select * from post_path;
+------------+--------------+--------+
| ancestorid | descendantid | length |
+------------+--------------+--------+
|          1 |            1 |      0 |
|          1 |            2 |      0 |
+------------+--------------+--------+
2 rows in set (0.00 sec)

After I issue the update statement, to update post.id to 20:

mysql> UPDATE `post` SET `id` = '20' WHERE `post`.`id` =2;
Query OK, 1 row affected (0.08 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from post_path;
+------------+--------------+--------+
| ancestorid | descendantid | length |
+------------+--------------+--------+
|          1 |            1 |      0 |
|          1 |           20 |      0 |
+------------+--------------+--------+
2 rows in set (0.00 sec)

Notice that the ancestorid is still 1, this appears to be an issue with MySQL:

If you use a multiple-table UPDATE statement involving InnoDB tables for which there are foreign key constraints, the MySQL optimizer might process tables in an order that differs from that of their parent/child relationship. In this case, the statement fails and rolls back. Instead, update a single table and rely on the ON UPDATE capabilities that InnoDB provides to cause the other tables to be modified accordingly. See Section 14.3.5.4, “InnoDB and FOREIGN KEY Constraints”.

The reason why your first query is failing, is because ancestorid is not updating to 10, but descendantid is, and because you are trying to set post.id to 10, and ancestorid in post_path table is still referencing the value 1, which would no longer exist.

You should consider altering your schema to avoid this, and to also avoid updating an auto_increment column so you avoid collisions.

like image 92
Mike Purcell Avatar answered Nov 19 '22 00:11

Mike Purcell


I believe the solution to your problem is to remove descendantid as a constraint and use a trigger to perform an update on the field.

delimiter $$
CREATE TRIGGER post_trigger
AFTER UPDATE ON post
FOR EACH ROW
BEGIN
UPDATE post_path SET post_path.descendantid = NEW.id WHERE post_path.descendantid = OLD.id
END$$
like image 40
blackmambo Avatar answered Nov 19 '22 01:11

blackmambo