简体   繁体   English

如何在MySQL中递归删除行(即删除外键链接的行)?

[英]How to delete rows recursively (i.e. also deleting foreign key linked rows) in MySQL?

I use Constraints in my MySQL Database. 我在MySQL数据库中使用约束。 But now it gives me a headache when I try to delete an entry on which other entries have a foreign-key relationship. 但是现在当我尝试删除其他条目具有外键关系的条目时,它让我头疼。 I always get this error: 我总是得到这个错误:

Cannot delete or update a parent row: a foreign key constraint fails

Can I pass the delete-statement any parameter or anything, so that it recursivly deletes all rows that have a foreign-key relationship to the row I'm trying to delete? 我可以传递delete-statement任何参数或任何东西,以便它递归删除与我试图删除的行有外键关系的所有行吗?

UPDATE: Have now made this into a blog post: https://stevettt.blogspot.co.uk/2018/02/how-to-automate-deletion-of-rows-in.html 更新:现在已将其变成博客文章: https//stevettt.blogspot.co.uk/2018/02/how-to-automate-deletion-of-rows-in.html


I've written a stored procedure that will recursively delete from all foreign key - linked tables (without needing to turn off foreign key checks or turn on cascade deletes). 我编写了一个存储过程,将从所有外键链接表中递归删除(无需关闭外键检查或打开级联删除)。 The implementation has some complexity but can be treated as a "black box": Simply specify the name of the schema (database), table and a WHERE clause to restrict the records to be deleted and it will do the rest. 实现有一些复杂性但可以被视为“黑盒子”:只需指定模式(数据库)的名称,表和WHERE子句来限制要删除的记录,它将完成剩下的工作。

Demo 演示

Rextester online demo: http://rextester.com/MDMRA15991 Rextester在线演示: http ://rextester.com/MDMRA15991

SQL SQL

-- ------------------------------------------------------------------------------------
-- USAGE
-- ------------------------------------------------------------------------------------
-- CALL delete_recursive(<schema name>, <table name>, <WHERE clause>, <delete flag>);
-- where:
-- <schema name> is the name of the MySQL schema
-- <table name> is the name of the base table to delete records from
-- <WHERE clase> is a SQL WHERE clause to filter which records that are to be deleted
-- <delete flag> is either TRUE or FALSE: If TRUE, the records *will* be deleted.
--               If FALSE, the SQL will be output without actually deleting anything.
-- Example:
-- CALL delete_recursive('mydb', 'mytable', 'WHERE mypk IN (1, 2, 3)', TRUE);
DROP PROCEDURE IF EXISTS delete_recursive;
DELIMITER //
CREATE PROCEDURE delete_recursive(schema_name VARCHAR(64),
                                  tbl_name VARCHAR(64),
                                  where_clause TEXT,
                                  do_delete BIT)
BEGIN
  DECLARE next_schema_name, next_tbl_name VARCHAR(64);
  DECLARE from_clause, next_where_clause, next_col_names, ref_col_names TEXT;
  DECLARE done INT DEFAULT FALSE;
  DECLARE cursor1 CURSOR FOR
    SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAMES, REF_COLUMN_NAMES FROM temp_kcu;
  DECLARE cursor2 CURSOR FOR
    SELECT table_schema, table_name, where_sql FROM temp_deletes ORDER BY id;
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;

  -- Set maximum recursion depth
  SET @@SESSION.max_sp_recursion_depth = 255;

  -- Increment current recursion depth since the stored procedure has been entered.
  SET @recursion_depth = IFNULL(@recursion_depth + 1, 0);

  -- Create temporary table for storing the deletes if it doesn't already exist
  IF @recursion_depth = 0 THEN
    DROP TEMPORARY TABLE IF EXISTS temp_deletes;
    CREATE TEMPORARY TABLE temp_deletes (
      id INT NOT NULL AUTO_INCREMENT,
      table_schema VARCHAR(64),
      table_name VARCHAR(64),
      where_sql TEXT,
      Notes TEXT,
      PRIMARY KEY(id)
    );
  END IF;

  -- Construct FROM clause (including the WHERE clause) for this table.
  SET from_clause = 
    CONCAT(' FROM ', schema_name, '.', tbl_name, ' WHERE ', where_clause);

  -- Find out whether there are any foreign keys to this table
  SET @query = CONCAT('SELECT COUNT(*) INTO @count', from_clause);
  PREPARE stmt FROM @query;
  EXECUTE stmt;
  DEALLOCATE PREPARE stmt;

  IF @count > 0 THEN
    -- There are foriegn keys to this table so all linked rows must be deleted first:
    -- Firstly, fill a temporary table with the foreign key metadata.
    DROP TEMPORARY TABLE IF EXISTS temp_kcu;
    SET @query = CONCAT(
      'CREATE TEMPORARY TABLE temp_kcu AS ',
      'SELECT TABLE_SCHEMA, TABLE_NAME, ',
      'GROUP_CONCAT(CONCAT(COLUMN_NAME) SEPARATOR '', '') AS COLUMN_NAMES, ', 
      'GROUP_CONCAT(CONCAT(REFERENCED_COLUMN_NAME) SEPARATOR '', '')
        AS REF_COLUMN_NAMES ',
      'FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE ',
      'WHERE REFERENCED_TABLE_SCHEMA = ''', schema_name,
      ''' AND REFERENCED_TABLE_NAME = ''', tbl_name, ''' ',
      'GROUP BY CONSTRAINT_NAME');
    PREPARE stmt FROM @query;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;

    -- Loop through all foreign keys to this table using a cursor.
    OPEN cursor1;
    read_loop: LOOP
      FETCH cursor1 INTO next_schema_name, next_tbl_name, next_col_names,
            ref_col_names;
      IF done THEN
        -- No more rows so exit the loop.
        LEAVE read_loop;
      END IF;

      -- Recursively call the stored procedure to delete linked rows
      -- for this foreign key.
      IF INSTR(next_col_names, ',') = 0 THEN
        SET next_where_clause = CONCAT(
          next_col_names, ' IN (SELECT ', ref_col_names, from_clause, ')');
      ELSE
        SET next_where_clause = CONCAT(
          '(', next_col_names, ') IN (SELECT ', ref_col_names, from_clause, ')');
      END IF;
      CALL delete_recursive(
        next_schema_name, next_tbl_name, next_where_clause, do_delete);
    END LOOP;
    CLOSE cursor1;
  END IF;

  -- Build the DELETE statement
  SET @query = CONCAT(
    'DELETE FROM ', schema_name, '.', tbl_name, ' WHERE ', where_clause);

  -- Get the number of primary key columns
  SET @pk_column_count = (SELECT COUNT(*)
                          FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
                          WHERE TABLE_SCHEMA = schema_name
                            AND TABLE_NAME = tbl_name
                            AND CONSTRAINT_NAME = 'PRIMARY');
  IF @pk_column_count = 0 THEN
    -- No primary key so just output the number of rows to be deleted
    SET @query = CONCAT(
      'SET @notes = CONCAT(''No primary key; number of rows to delete = '',
      (SELECT COUNT(*) FROM ', schema_name, '.', tbl_name, ' WHERE ',
      where_clause, '))');
    PREPARE stmt FROM @query;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
  ELSEIF @pk_column_count = 1 THEN
    -- 1 primary key column.
    -- Output the primary keys of the records to be deleted
    SET @pk_column = (SELECT COLUMN_NAME
                      FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
                      WHERE TABLE_SCHEMA = schema_name
                        AND TABLE_NAME = tbl_name
                        AND CONSTRAINT_NAME = 'PRIMARY');
    SET @pk_column_csv = CONCAT('CONCAT('''''''', ', @pk_column, ', '''''''')');
    SET @query = CONCAT(
      'SET @notes = (SELECT CONCAT(''', @pk_column, ' IN ('', GROUP_CONCAT(',
      @pk_column_csv, ' SEPARATOR '', ''), '')'') FROM ',
      schema_name, '.', tbl_name, ' WHERE ', where_clause, ')');
    PREPARE stmt FROM @query;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
  ELSE
    -- Multiple primary key columns.
    -- Output the primary keys of the records to be deleted.
    SET @pk_columns = (SELECT GROUP_CONCAT(COLUMN_NAME SEPARATOR ', ')
                       FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
                       WHERE TABLE_SCHEMA = schema_name
                         AND TABLE_NAME = tbl_name
                         AND CONSTRAINT_NAME = 'PRIMARY');
    SET @pk_columns_csv = (SELECT CONCAT('CONCAT(''('''''', ', GROUP_CONCAT(COLUMN_NAME
                             SEPARATOR ', '''''', '''''', '), ', '''''')'')')
                           FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
                           WHERE TABLE_SCHEMA = schema_name
                             AND TABLE_NAME = tbl_name
                             AND CONSTRAINT_NAME = 'PRIMARY');      
    SET @query = CONCAT(
     'SET @notes = (SELECT CONCAT(''(', @pk_columns,
     ') IN ('', GROUP_CONCAT(', @pk_columns_csv, ' SEPARATOR '', ''), '')'') FROM ',
      schema_name, '.', tbl_name, ' WHERE ', where_clause, ')');
    PREPARE stmt FROM @query;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
  END IF;

  IF @notes IS NULL THEN
    SET @notes = 'No affected rows.';
  END IF;

  -- Save details of the DELETE statement to be executed
  INSERT INTO temp_deletes (table_schema, table_name, where_sql, Notes)
  VALUES (schema_name, tbl_name, where_clause, @notes);

  IF @recursion_depth = 0 THEN
    -- Output the deletes.
    SELECT CONCAT('DELETE FROM ', schema_name, '.', table_name,
                  ' WHERE ', where_sql) `SQL`,
           Notes
    FROM temp_deletes ORDER BY id;

    IF do_delete THEN
      -- Perform the deletes: Loop through all delete queries using a cursor.
      SET done = FALSE;
      OPEN cursor2;
      read_loop: LOOP
        FETCH cursor2 INTO schema_name, tbl_name, where_clause;
        IF done THEN
          -- No more rows so exit the loop.
          LEAVE read_loop;
        END IF;

        SET @query = CONCAT(
          'DELETE FROM ', schema_name, '.', tbl_name, ' WHERE ', where_clause);

        PREPARE stmt FROM @query;
        EXECUTE stmt;
        DEALLOCATE PREPARE stmt;
      END LOOP;
      CLOSE cursor2;
    END IF;

    -- Tidy up
    DROP TEMPORARY TABLE IF EXISTS temp_deletes;
  END IF;

  -- Decrement current recursion depth since the stored procedure is being exited.
  SET @recursion_depth = @recursion_depth - 1;
END;//
DELIMITER ;

Limitations 限制

  1. CREATE TEMPORARY TABLES permission is required for the user running the stored procedure for the schema(s) being used. 对于正在使用的模式运行存储过程的用户,需要CREATE TEMPORARY TABLES权限。
  2. MySQL only supports a maximum recursion depth of 255 so this method would fall over if there were a very large number of foreign key links (seems unlikely). MySQL只支持255的最大递归深度,因此如果有非常多的外键链接(似乎不太可能),这种方法就会失败。
  3. "Circular" / "cyclic" foreign key references (eg table A has a foreign key to table B and table B has a foreign key back to table A) are not currently supported and would cause an infinite loop. “循环”/“循环”外键引用(例如,表A具有表B的外键,而表B具有返回表A的外键)当前不受支持并且将导致无限循环。
  4. It's not designed for use on a "live" system: Since data is deleted recursively, later deletions could fail if it happened that more data was added between deleting child and parent records. 它不是设计用于“实时”系统:由于数据是递归删除的,如果在删除子记录和父记录之间添加了更多数据,以后删除可能会失败。

Look at this: 看这个:

In what order are ON DELETE CASCADE constraints processed? 处理ON DELETE CASCADE约束的顺序是什么?

But I think you can use ON DELETE CASCADE from my research. 但我认为你可以使用我的研究中的ON DELETE CASCADE。 If I am wrong I am sure the community will let me know. 如果我错了,我相信社区会告诉我。 I believe you will have to alter your tables, if possible. 如果可能的话,我相信你必须改变你的桌子。

Also see this: 另见:

Cannot delete or update a parent row: a foreign key constraint fails 无法删除或更新父行:外键约束失败

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何调试mysql外键约束,ON DELETE CASCADE不能在生产环境中从子表中删除行 - How to debug mysql foreign key constraint, ON DELETE CASCADE not deleting rows from Child table on production environment 如何删除外键为NULL的行? - How can I delete rows where its foreign key is NULL? 如何从与外键连接的表中删除行? - How can I delete rows from table connected with foreign key? MySQL返回一个空结果集(即零行) - MySQL returned an empty result set (i.e. zero rows) 如何从查询本身获取行数(即没有mysql_num_rows()或FOUND_ROWS()) - how to get the rowcount from the query itself (i.e. without mysql_num_rows() or FOUND_ROWS()) MySQL试图删除所有不受外键约束的行 - MySQL attempting to delete all rows which are not constrained by foreign key MySQL返回一个空结果集(即零行),其中期望的结果多于零行 - MySQL returned an empty result set (i.e. zero rows) where expecting more then zero rows 如何从与外键链接的两个表中选择不同的行 - how to select distinct rows from two tables linked with foreign key MySQL插入表外键列条件插入,即存在或不存在 - MySQL insert into table foreign key column conditional insert i.e. exist or doesn't exist 如何与外键关系一起删除两行 - How to delete two rows together with foreign key relationship
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM