如何在MySQL中递归删除行(即也删除外键链接行)?
我在MySQL数据库中使用约束。但现在,当我试图删除一个其他条目具有外键关系的条目时,它让我头疼。我总是会遇到这样的错误:如何在MySQL中递归删除行(即也删除外键链接行)?,mysql,foreign-keys,constraints,sql-delete,recursive-query,Mysql,Foreign Keys,Constraints,Sql Delete,Recursive Query,我在MySQL数据库中使用约束。但现在,当我试图删除一个其他条目具有外键关系的条目时,它让我头疼。我总是会遇到这样的错误: Cannot delete or update a parent row: a foreign key constraint fails 我是否可以向delete语句传递任何参数或任何内容,以便它递归删除与我要删除的行具有外键关系的所有行?请看以下内容: 但我想你可以从我的研究中删除CASCADE。如果我错了,我相信社区会让我知道。如果可能的话,我相信你必须改变你的桌子
Cannot delete or update a parent row: a foreign key constraint fails
我是否可以向delete语句传递任何参数或任何内容,以便它递归删除与我要删除的行具有外键关系的所有行?请看以下内容:
但我想你可以从我的研究中删除CASCADE。如果我错了,我相信社区会让我知道。如果可能的话,我相信你必须改变你的桌子
另请参见:
更新:现已将此内容写入博客帖子:
我编写了一个存储过程,它将递归地从所有外键链接表中删除,而无需关闭外键检查或打开级联删除。该实现有一些复杂性,但可以被视为一个黑盒:只需指定模式数据库、表和WHERE子句的名称,以限制要删除的记录,其余的就可以完成
演示
Rextester在线演示:
SQL
局限性
对于正在使用的架构,运行存储过程的用户需要权限。
MySQL只支持255的最大递归深度,因此如果存在大量外键链接,则该方法将失效。
当前不支持循环/循环外键引用,例如表A具有表B的外键,表B具有返回表A的外键,这将导致无限循环。
它不是为在实时系统上使用而设计的:因为数据是递归删除的,如果在删除子记录和父记录之间添加了更多数据,则以后的删除可能会失败。
我想他要求的是delete语句的参数/变量,而不是ON delete级联模式更改。脚本不错。但是如果能够支持循环键引用就好了,因为这种用例并不罕见。你考虑过了吗?如果是,您能告诉我吗?希望通过创建另一个临时表来存储迄今为止已处理的所有外键的标识符,然后确保已处理的外键在再次遇到时不会被处理,这是可行的。目前我还没有时间调查此事,但我会把它列入我的待办事项清单。
-- ------------------------------------------------------------------------------------
-- 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 ;