假设我们有一个像这样的 PostgreSQL 表:
CREATE TABLE master (
id INT PRIMARY KEY,
...
);
以及许多其他使用外键引用它的表:
CREATE TABLE other (
id INT PRIMARY KEY,
id_master INT NOT NULL,
...
CONSTRAINT other_id_master_fkey FOREIGN KEY (id_master)
REFERENCES master (id) ON DELETE RESTRICT
);
有没有办法(从触发器函数内)检查主行是否可删除,而不实际尝试删除它?最明显的方法是对所有引用表一一进行 SELECT,但我想知道是否有更简单的方法。
我需要这个的原因是,我有一个包含分层数据的表,其中任何行都可以有子行,并且只有层次结构最低的子行可以被其他表引用。因此,当一行即将成为父行时,我需要检查它是否已在任何地方被引用。如果是,则它不能成为父行,并且拒绝插入新的子行。
粗体EXCEPTION
子句捕获错误时,局部变量 PL/pgSQL 函数保持错误发生时的状态,
但是 块内对持久数据库状态的所有更改都会回滚。
强调我的。 但是您可以将其包装到单独的块或单独的
plpgsql 函数中,并在那里捕获异常以防止对主(触发)函数产生影响。
CREATE OR REPLACE FUNCTION f_can_del(_id int)
RETURNS boolean AS
$func$
BEGIN
DELETE FROM master WHERE master_id = _id; -- DELETE is always rolled back
IF NOT FOUND THEN
RETURN NULL; -- ID not found, return NULL
END IF;
RAISE SQLSTATE 'MYERR'; -- If DELETE, raise custom exception
EXCEPTION
WHEN FOREIGN_KEY_VIOLATION THEN
RETURN FALSE;
WHEN SQLSTATE 'MYERR' THEN
RETURN TRUE;
-- other exceptions are propagated as usual
END
$func$ LANGUAGE plpgsql;
这将返回
TRUE
/
FALSE
/ NULL
表示该行可以删除/不可删除/不存在。db<>小提琴这里
旧
sqlfiddle
可以轻松地使此函数动态化以测试任何表/列/值。
自
PostgreSQL 9.2 起,您还可以报告哪个表被阻塞。
PostgreSQL 9.3
或更高版本提供了更详细的信息。
任意表、列和类型的通用函数
失败了? 手册中的这句话应该给出一个线索:
特别注意,EXECUTE
更改了
的输出,但不更改GET DIAGNOSTICS
。 它适用于FOUND
GET DIAGNOSTICS
:
CREATE OR REPLACE FUNCTION f_can_del(_tbl regclass, _col text, _id int)
RETURNS boolean AS
$func$
DECLARE
_ct int; -- to receive count of deleted rows
BEGIN
EXECUTE format('DELETE FROM %s WHERE %I = $1', _tbl, _col)
USING _id; -- exception if other rows depend
GET DIAGNOSTICS _ct = ROW_COUNT;
IF _ct > 0 THEN
RAISE SQLSTATE 'MYERR'; -- If DELETE, raise custom exception
ELSE
RETURN NULL; -- ID not found, return NULL
END IF;
EXCEPTION
WHEN FOREIGN_KEY_VIOLATION THEN
RETURN FALSE;
WHEN SQLSTATE 'MYERR' THEN
RETURN TRUE;
-- other exceptions are propagated as usual
END
$func$ LANGUAGE plpgsql;
db<>小提琴这里
旧
sqlfiddle
在这个过程中,我将其设置为“完全动态”,包括列的数据类型(当然,它必须与给定的列匹配)。为此,我使用
format()
regclass
类型的参数来防范 SQLi。参见:
Postgres 函数中的 SQL 注入与准备好的查询Procedure
CREATE OR REPLACE procedure p_delable(_tbl text, _col text, _id int)
AS $$
DECLARE
_ct bigint;
_exists boolean; -- to receive count of deleted rows
BEGIN
_exists := (SELECT EXISTS ( SELECT FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = $1 ));
IF _exists THEN
EXECUTE format('DELETE FROM %s WHERE %I = $1', _tbl, _col)
USING _id; -- exception if other rows depend
GET DIAGNOSTICS _ct = ROW_COUNT;
IF _ct > 0 THEN
RAISE SQLSTATE 'MYERR'; -- If DELETE, raise custom exception
ELSE
RAISE NOTICE 'no records found. no records will be deleted';
END IF;
ELSE
raise notice 'Input text is invalid table name.';
END IF;
EXCEPTION
WHEN undefined_column then
raise notice 'Input text is invalid column name.';
WHEN undefined_table then
raise notice 'Input text is invalid table name.';
WHEN FOREIGN_KEY_VIOLATION THEN
RAISE NOTICE 'foreign key violation, cannot be deleted.';
WHEN SQLSTATE 'MYERR' THEN
RAISE NOTICE 'rows % found and can be deleted.', _ct;
END
$$ LANGUAGE plpgsql;
call p_delable('parent_tree', 'parent_id',30);
将会得到:
NOTICE: no records found. no records will be deleted
让我们尝试一个实际存在的行。
call p_delable('parent_tree', 'parent_id',3);
它会回来的
NOTICE: rows 1 found and can be deleted.
它还可以检查您的输入表名称是否存在于
public
模式中。
call p_delable('parent_tre', 'parent_id',3);
NOTICE: Input text is invalid table name.
如果存在外键 ON DELETE RESTRICT 约束,该约束禁止删除被推迟的行(主要是因为该约束是可推迟的并且最初是推迟的),则尝试删除和回滚将不起作用。您没有得到任何信息
也许有人必须编写一个功能请求来向 postgresql 添加 SQL 命令,该请求检查实际(子)事务上的 COMMIT 是否不会失败(包括检查重复键违规、约束违规、范围违规以及所有 lametta,但这样做没有真正的承诺)。