引用完整性教程 & 引用完整性表的破解
作者:Joel Burton
最后更新时间:2001 年 9 月 3 日
介绍
引用完整性是数据库的一个功能,它确保数据库中隐含的关系得到强制执行。它是大多数数据库系统的功能,可以保护用户免受意外(或有意!)创建数据库中不一致的风险。
在本章中,我们将使用一组示例数据,其中包含人员、他们负责的任务以及与他们安排的约会。当您解雇员工时,您会将他们从您的人员表中删除,但希望确保您无法留下没有人负责的任务,或留下找不到人员姓名的时间安排。
如果您不熟悉主键和外键的概念,建议您复习一下基本的数据库入门知识,因为本教程假设您已经了解这些概念。
让我们开始吧!
首先,因为我们将使用系统表,最好在一个练习数据库中进行
# CREATE DATABASE RI_test; # \c ri_test
现在,让我们创建练习表。我们将有一个父表“pers”和两个子表“tasks”和“appts”。
父表很简单
# CREATE TABLE pers (pid INT NOT NULL PRIMARY KEY, pname TEXT NOT NULL);
第一个子表将保存人员负责的任务
# CREATE TABLE tasks (taskid SERIAL NOT NULL PRIMARY KEY, pid INT NOT NULL CONSTRAINT tasks__ref_p REFERENCES pers, task TEXT NOT NULL);
几点说明
- 外键引用由 PostgreSQL 作为一种约束处理。因此,我们可以用 CONSTRAINT <约束名称> 开头引用声明。如果这样做,PostgreSQL 生成的错误消息将返回此错误名称,用户可以方便地看到它,或者我们可以在前端系统中解析和处理它。通过将其包装在系统标识符(")中,我们甚至可以使其看起来像一条错误消息(即 CONSTRAINT "Table tasks refers to table pers"),但我们对此名称/消息的长度有限,因此它不那么有用。约束名称不是必需的,我们可以省略它(即 ...pid INT NOT NULL REFERENCES pers...)。一切仍然相同,除了我们的约束没有名称,因此错误消息更加通用。
- 在 REFERENCES 后面是我们要引用的表的名称,以及可选的括号中的字段名称。如果该字段是主键,则字段名称是可选的。如果我们没有将 'pid' 字段设置为 'pers' 的主键,我们将必须使用 'REFERENCES pers(pid)'。
- 我们可以选择通过添加 ON DELETE 和 ON UPDATE 声明来通知 PostgreSQL 如何处理表 'pers' 上的删除和更新。如果这些没有被具体指定,那么 PostgreSQL 默认使用 “no action”,这(在撰写本文时,2000 年 12 月)与 “restrict” 相同。这意味着如果存在依赖于父项的子项,我们既不能删除父项,也不能更新父项的 'pid' 字段。在我们的另一个子表中,我们将看到另一种处理方法。
- 我们可以选择告诉 PostgreSQL 这是否是 “可延迟的”,也就是说,如果我们允许在事务内部违反引用完整性,只要我们在事务结束时修复了我们的完整性问题。我们将在下面的 DEFERRING 中看到此示例。
因此,让我们添加一些示例数据
INSERT INTO pers VALUES (1, 'Jeff Brown'); INSERT INTO pers VALUES (2, 'Maria Lane'); INSERT INTO tasks (pid, task) VALUES (1, 'Write contract'); INSERT INTO tasks (pid, task) VALUES (1, 'Upgrade database');
到目前为止一切顺利。
如果我们尝试插入一个没有父项的子项,即
INSERT INTO tasks (pid, task) VALUES (3, 'Install Linux');
我们将收到一个引用完整性错误。此错误将引用我们的约束名称(如果有),并将阻止将此数据输入 'tasks'。此外,如果这是事务的一部分,它将回滚整个事务,如 PostgreSQL 中始终发生的那样。
此外,如果我们尝试更改一个有子项的父项
DELETE FROM pers WHERE pname = 'Jeff Brown';
我们将遇到同样的问题,因为我们无法在 Jeff 被分配了任务的情况下删除他。
这将起作用,因为目前 Maria 没有子任务
DELETE FROM pers WHERE pname = 'Maria Lane';
如果您执行了上述步骤,您需要添加 Maria 回来以供后面的示例使用
INSERT INTO pers VALUES (2, 'Maria Lane');
默认(NO ACTION)或 RESTRICT - 对 PostgreSQL 来说意味着相同的事 - ON UPDATE 和 ON DELETE 规则在许多情况下是有意义的。您不希望能够从您的数据库中删除一名员工,即使他们拥有某些责任,否则您将永远不知道谁负责不同的任务。
一个更宽容的子表:约会
让我们创建一个第二个子表 'appts'(Appointment 的缩写)
# CREATE TABLE appts (apptid SERIAL NOT NULL PRIMARY KEY, pid INT NOT NULL CONSTRAINT appt__ref_pers REFERENCES pers ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE, apptsubj TEXT NOT NULL, apptdate DATE NOT NULL);
并将一些数据插入其中
INSERT INTO appts (pid, apptsubj, apptdate) VALUES (1, 'Discuss raise', '2001-01-15'); INSERT INTO appts (pid, apptsubj, apptdate) VALUES (2, 'Plan project', '2001-01-20');
这完全可以正常工作。同样,和之前一样
INSERT INTO appts (pid, apptsubj, apptdate) VALUES (3, 'Termination', '2001-01-20');
无法工作,因为 'pers' 表中不存在 pid = 3。
然而,与 'tasks' 表不同,'appts' 被设置为级联删除和更新。这意味着如果我们更新了人员的 pid,或完全删除了人员,PostgreSQL 将允许并协助此操作,方法是首先删除或更新该人员存在的任何任务。
因此
UPDATE pers SET pid=14 WHERE pid=2;
这将把 Maria 的 pid 更改为 14,首先更改 'appts' 表中连接的 pid。
DELETE FROM pers WHERE pid=14;
这将删除 Maria 的约会,然后删除 Maria。
其他关系
请注意,此类中的级联仅指 'pers' 和 'appt' 表之间的级联。尝试删除 Jeff 仍然会失败,因为即使 pers-appt 关系会级联,pers-tasks 关系也会失败,并且 PostgreSQL 会将删除尝试报告为违反此引用完整性。可能的动作
- NO ACTION(默认值)
如果更新或删除会失败引用完整性检查,则停止该操作。
- RESTRICT
目前与 NO ACTION 含义相同。
- CASCADE
删除子数据,然后删除父数据,这取决于其他关系,如上所述。
- SET NULL
将外键字段设置为 NULL,然后更新或删除父项。
- SET DEFAULT
将外键字段设置为其默认值,然后更新或删除父项。
SET NULL 和 SET DEFAULT 是有用的选项,特别是对于 ON DELETE。例如,我们可能有一个表 'offices',用于跟踪员工使用哪个办公室。它可能包含 officeid、officelocation 和 persid 字段。如果我们想删除一个人,我们不应该仅仅因为这个人有一个相关的办公室而被阻止,但同样,我们也不希望仅仅因为这个人被删除而删除一个办公室。在某些情况下,最好的选择可能是将 'offices' 的 persid 字段设置为 NULL 或 DEFAULT,使办公室保持原样,但明确表明该办公室现在没有被使用。
请注意,ON UPDATE 和 ON DELETE 可以有不同的规则。例如,ON UPDATE CASCADE 但 ON DELETE RESTRICT 非常常见 - 允许人们更改他们的 ID,但不允许在存在相关人员的情况下删除任务。
还可以更改关系的动作,这需要在系统目录表中进行一些破解。请参阅下面的破解引用完整性。
延迟事务
默认情况下,会对每一种关系的每一次插入、删除或更新进行引用完整性检查,这些操作可能会影响这种关系。
这意味着
INSERT INTO tasks (pid, task) VALUES (5, 'Open sales office'); INSERT INTO pers (pid, name) VALUES (5, 'Helen Kim');
会失败,因为在尝试第一次插入时,不存在 pid=5 的人员。在大多数情况下,这是最直观的设置。
但是,有时您可能无法预测数据到达的确切顺序。也许您正在接收从文本文件或网络加载的数据。数据可能不会按照上面的顺序到达(任务,然后是关联的人员)。在这种情况下,您可以选择延迟事务检查。
为了延迟事务,必须发生三件事
- 引用完整性关系必须被定义为 DEFERRABLE。这不是默认值(NOT DEFERRABLE 是),因此必须显式声明,如上文针对 'appts' 所做的那样。
- 您必须在显式事务中。
- 您必须让此关系的初始默认值为 DEFERRED,或者为当前事务将关系约束设置为 DEFERRED。
对于表 'appts' 与 'pers' 的关系,我们将其声明为 DEFERRED。因此,如果我们这样做
BEGIN; SET CONSTRAINTS ALL DEFERRED; INSERT INTO appts (pid, apptsubj, apptdate) VALUES (5, 'Negotiation', '2001-01-20'); INSERT INTO pers VALUES (5, 'Helen Kim'); COMMIT;
这将正常工作。
请注意,将关系声明为 DEFERRABLE 还不够 - 我们还必须使用 SET 将 CONSTRAINTS 显式设置为延迟。在此示例中,我们将所有关系都设置为 DEFERRED;相反,我们可以仅将单个约束设置为延迟,如下所示
SET CONSTRAINTS appts__ref_pers DEFERRABLE;
预先设置关系可能很方便,而不是每次事务都必须设置它。为此,在 CONSTRAINT ... REFERENCES 声明中为表添加 "INITIALLY DEFERRED"。
例如
# CREATE TABLE appts (apptid SERIAL NOT NULL PRIMARY KEY, pid INT NOT NULL CONSTRAINT appt__ref_pers REFERENCES pers ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, apptsubj TEXT NOT NULL, apptdate DATE NOT NULL);
现在,我们可以简单地
BEGIN; INSERT INTO appts (pid, apptsubj, apptdate) VALUES (5, 'Negotiation', '2001-01-20'); INSERT INTO pers VALUES (5, 'Helen Kim'); COMMIT;
无需显式 SET 命令。如果在抽象 SQL 命令的编程环境中工作,并且很难执行任意、非标准的 SQL 命令(例如 “SET...”),则这尤其方便。
破解引用完整性
引用完整性在 PostgreSQL 中非常有效。但是,PostgreSQL 还没有 SQL 语法命令来更改操作、打开/关闭引用完整性等。
可以通过直接编辑存储在系统目录表中的信息来执行这些操作。
警告
要编辑系统目录表,您必须是 PostgreSQL 中的超级用户。此外,编辑这些表时要非常小心,确保先进行备份(通过 pg_dump)。意外的表级 UPDATE 或 DELETE 可能会删除所有表、破坏索引、损坏数据库等。通过在测试数据库中进行操作来练习,最好在没有其他重要数据库的机器上进行。
我们感兴趣的几个系统目录表
pg_class
所有 “类”,包括表、视图、序列等。
pg_trigger
所有触发器。PostgreSQL 使用幕后触发器处理引用完整性,因此这里存储了所有引用完整性控制。
pg_proc
所有 PostgreSQL 过程。我们不需要对此进行任何更改,但我们将使用它来显示使用的引用完整性过程实际上是什么。
要查看(大多数)系统表,可以使用 psql 中的 \dS 命令。
例如,让我们从 pg_class 中收集有关测试表的信息
# SELECT oid, relname FROM pg_class WHERE relname IN ('pers','appts','tasks');
PostgreSQL 通常会自动将未加引号的系统标识符(如表)转换为小写(即我可以 “CREATE TABLE foo” 和 “SELECT * FROM FOO” 仍然有效)。但是,在检查 pg_class 时,您必须以区分大小写的形式工作,或者使用不区分大小写的运算符。
此查询返回
oid relname 9100 pers 9110 tasks 9120 appts (3 rows)
您的 OID 将不同。不要担心,但请注意它们是什么。
现在,如果我们在 pg_trigger 中查找,我们可以找到表使用的触发器。
- SELECT * from pg_trigger WHERE tgrelid in (9100, 9110, 9120);
请注意,此命令的输出表太宽,无法放入此 HTML 页面中。您可以在此处找到它。
pg_trigger 中的列是
tgrelid
触发器所在的表。插入/更新/删除并调用触发器的表。
tgname
触发器的名称。触发器名称由PostgreSQL的参照完整性功能生成,除了要唯一之外,它们并不重要。
tgfoid
要调用的函数的OID。这是一列重要的列;通过更改调用的函数,我们可以更改执行的操作(CASCADE等)。见下文。
tgtype
这是什么类型的触发器?UPDATE、DELETE等。
tgenabled
此触发器是否已启用?
tgisconstraint
此触发器是否是约束的一部分?非参照完整性触发器(由CREATE TRIGGER用户声明)对于此可能为FALSE,但参照完整性触发器将始终为true。
tgconstrname
调用此触发器的约束的名称。如果您为约束命名(就像我们一样,例如pers__ref_tasks),那么这将是约束名称,否则它将是未命名的。
tgconstrrelid
设置约束的表的OID。
tgdeferrable
此约束是否可以延迟?等同于DEFERRABLE。见上文。
tginitdeferred
此约束是否最初延迟?等同于INITIALLY DEFERRED。见上文。
tgnargs
参照完整性函数的参数数量。从PostgreSQL 7.1开始,这似乎始终为6,不应该被编辑。
tgattr
不确定
tgargs
参照完整性函数的实际参数。
为了使它更容易理解和使用,视图很有帮助。
CREATE VIEW dev_ri AS SELECT t.oid as trigoid, c.relname as trig_tbl, t.tgfoid, f.proname as trigfunc, t.tgenabled, t.tgconstrname, c2.relname as const_tbl, t.tgdeferrable, t.tginitdeferred FROM pg_trigger t, pg_class c, pg_class c2, pg_proc f WHERE t.tgrelid=c.oid AND t.tgconstrrelid=c2.oid AND tgfoid=f.oid AND tgname ~ '^RI_' ORDER BY t.oid;
此视图需要PostgreSQL 7.1,因为使用了ORDER BY语句。对于早于7.1的PostgreSQL版本,您应该删除ORDER BY语句。
# select * from dev_ri;
请注意,此命令的输出表太宽,无法放入此 HTML 页面中。您可以在此处找到它。
现在,更容易理解正在发生的事情。例如,从触发器oid=263753,我们可以看到对'tasks'表的插入,调用了RI_FKey_check_ins,它检查了'pers'表。
从这里,我们可以
1)暂时禁用触发器。如果你想加载大量数据,并且不想因为参照完整性检查而减慢速度(并且不需要处理事务级延迟)
UPDATE pg_trigger SET tgenabled=FALSE WHERE oid=xxx;
从我们的视图中获取trigoid列,以找到要影响的触发器。
请注意,pg_dump 文件会这样做,以便可以按任意顺序插入表数据,而不必担心参照完整性规则。
2)如果一个触发器最初不是可延迟的,则将其设置为可延迟的,反之亦然。
UPDATE pg_trigger SET tgdeferrable=[ TRUE | FALSE ] WHERE oid=xxx;
或者,使触发器INITIALLY DEFERRED(或将其关闭)
UPDATE pg_trigger SET tginitdeferred=[ TRUE | FALSE ] WHERE oid=xxx;
3)更改触发器的操作。如果您创建了一个带有操作的触发器(或带有默认的 NO ACTION 操作),您可以通过更改调用的函数来改变主意。要获取所有参照完整性触发器函数的列表
SELECT oid, proname FROM pg_proc where proname ~ '^RI_'; oid proname 1646 RI_FKey_cascade_del 1647 RI_FKey_cascade_upd 1644 RI_FKey_check_ins 1645 RI_FKey_check_upd 1654 RI_FKey_noaction_del 1655 RI_FKey_noaction_upd 1648 RI_FKey_restrict_del 1649 RI_FKey_restrict_upd 1652 RI_FKey_setdefault_del 1653 RI_FKey_setdefault_upd 1650 RI_FKey_setnull_del 1651 RI_FKey_setnull_upd
您的OID可能不同。注意并使用您自己的。
在我们的示例中,将pers(pid)上的更新设置为CASCADE,而不是pers-tasks上的NO ACTION
UPDATE pg_trigger SET tgfoid=1647 WHERE oid=xxx;
其中xxx是当前pers-tasks的noaction_upd触发器
请注意,这些更改通常需要新的后端。退出并重新启动psql,或者重置您的客户端连接,您应该能够测试您的新设置。