数据库表之间的某些相互依赖性无法(轻松)仅使用外键和检查约束进行建模.在我目前的项目中,我已经开始为所有这些条件编写约束触发器,但看起来如果我走这条路线,我将最终得到数百个触发器.
我的主要问题是:
>下面概述的场景中的触发器和约束是否实际覆盖了所有基础,或者是否仍然可以以结果不一致的方式添加/修改数据?
>写下所有这些触发器真的是正确的方法吗?我很少在第三方数据库模式中看到约束触发器.其他人是否只是相信应用程序不会搞砸?
最小的示例场景
中央“库存”表包含所有跟踪的项目.一些库存物品具有特定尺寸的特定类型;这些额外的尺寸存储在单独的表格中(“书籍”,“图片”).这个基本的表格布局不能改变(这只是一个例子;实际的DB显然有更多的表和列).
额外要求:
(A)“库存”表中类型为“book”的每一行必须在“books”中有一个匹配的行(“pictures”相同)
(B)“书籍”表中的每一行必须指向“库存”中的唯一行,其类型为“书”(“图片”相同)
(C)插入后,“库存”记录永远不会改变其类型
完整的数据库内容
"inventory": id | type | name
----+------+----------------------
a | pic | panda.jpg
b | book | How to do stuff
c | misc | ball of wool
d | book | The life of π
e | pic | Self portrait (1889)
"pictures": inv_id | quality
--------+----------------------------
a | b/w photo?
e | nice, but missing right ear
"books": inv_id | author
--------+--------
b | Hiro P
d | Yann M
创建并填充架构:
CREATE TABLE inventory (
id CHAR(1) PRIMARY KEY,
type TEXT NOT NULL CHECK (type IN ('pic', 'book', 'misc')),
name TEXT NOT NULL
);
CREATE TABLE pictures (
inv_id CHAR(1) PRIMARY KEY REFERENCES inventory(id) ON UPDATE CASCADE ON DELETE CASCADE,
quality TEXT
);
CREATE TABLE books (
inv_id CHAR(1) PRIMARY KEY REFERENCES inventory(id) ON UPDATE CASCADE ON DELETE CASCADE,
author TEXT
);
INSERT INTO inventory VALUES
('a', 'pic', 'panda.jpg'),
('b', 'book', 'How to do stuff'),
('c', 'misc', 'ball of wool'),
('d', 'book', 'The life of π'),
('e', 'pic', 'Self portrait (1889)');
INSERT INTO pictures VALUES
('a', 'b/w photo?'),
('e', 'nice, but missing right ear');
INSERT INTO books VALUES
('b', 'Hiro P'),
('d', 'Yann M');
添加触发器以维护跨表一致性:
-- TRIGGER: if inventory.type is 'book', there must be a corresponding record in
-- "books" (provides A, 1/2)
CREATE FUNCTION trg_inventory_insert_check_details () RETURNS TRIGGER AS $fun$
DECLARE
type_table_map HSTORE := hstore(ARRAY[
['book', 'books'],
['pic', 'pictures'] -- etc...
]);
details_table TEXT;
num_details INT;
BEGIN
IF type_table_map ? NEW.type THEN
details_table := type_table_map->(NEW.type);
EXECUTE 'SELECT count(*) FROM ' || details_table::REGCLASS || ' WHERE inv_id = $1'
INTO num_details
USING NEW.id;
IF num_details != 1 THEN
RAISE EXCEPTION 'A new "%"-type inventory record also needs a record in "%".',
NEW.type, details_table;
END IF;
END IF;
RETURN NULL;
END;
$fun$LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER insert_may_require_details
AFTER INSERT ON inventory
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE trg_inventory_insert_check_details();
-- TRIGGER: when deleting details, parent must be gone, too (provides A, 2/2)
CREATE FUNCTION trg_inv_details_delete () RETURNS TRIGGER AS $fun$
BEGIN
IF EXISTS(SELECT 1 FROM inventory WHERE id = OLD.inv_id) THEN
RAISE EXCEPTION 'Cannot delete "%" record without deleting inventory record (id=%).',
TG_TABLE_NAME, OLD.inv_id;
END IF;
RETURN NULL;
END;
$fun$LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER delete_parent_too
AFTER DELETE ON books
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE trg_inv_details_delete();
CREATE CONSTRAINT TRIGGER delete_parent_too
AFTER DELETE ON pictures
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE trg_inv_details_delete();
-- TRIGGER: details records must point to the correct inventory type (provides B)
CREATE FUNCTION trg_inv_details_check_parent_type () RETURNS TRIGGER AS $fun$
DECLARE
table_type_map HSTORE := hstore(ARRAY[
['books', 'book'],
['pictures', 'pic'] -- etc...
]);
required_type TEXT;
p_type TEXT;
BEGIN
required_type := table_type_map->(TG_TABLE_NAME);
SELECT type INTO p_type FROM inventory WHERE id = NEW.inv_id;
IF p_type != required_type THEN
RAISE EXCEPTION '%.inv_id (%) must point to an inventory item with type="%".',
TG_TABLE_NAME, NEW.inv_id, required_type;
END IF;
RETURN NULL;
END;
$fun$LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER check_parent_inv_type
AFTER INSERT OR UPDATE ON books
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE trg_inv_details_check_parent_type();
CREATE CONSTRAINT TRIGGER check_parent_inv_type
AFTER INSERT OR UPDATE ON pictures
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE trg_inv_details_check_parent_type();
-- TRIGGER: value of inventory.type cannot be changed (provides C)
CREATE FUNCTION trg_fixed_cols () RETURNS TRIGGER AS $fun$
DECLARE
old_rec HSTORE := hstore(OLD);
new_rec HSTORE := hstore(NEW);
col TEXT;
BEGIN
FOREACH col IN ARRAY TG_ARGV LOOP
IF NOT (old_rec ? col) THEN
RAISE EXCEPTION 'Column "%.%" does not exist.', TG_TABLE_NAME, col;
ELSIF (old_rec->col) != (new_rec->col) THEN
RAISE EXCEPTION 'Column "%.%" cannot be modified.', TG_TABLE_NAME, col;
END IF;
END LOOP;
RETURN NULL;
END;
$fun$LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER fixed_cols
AFTER UPDATE ON inventory
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE trg_fixed_cols('type');
最佳答案 Zilk,这里的问题是你首先要说的是架构无法修改.您需要编写此触发器spaghetti代码的原因是因为业务逻辑和架构的设计不一致.例如,trg_inv_details_delete正在尖叫我,只是试图重新发明外键参照完整性 – 像Postgres这样的DBMS为你做的事情.
这实际上是一个非常典型的子类/超类层次结构问题,需要在数据库的设计阶段解决.您试图表达的实际迷你世界的细节将决定您如何对此进行建模.从那个知识将来到enhanced ERD,然后你将这个概念模型转换为逻辑模式.
在此特定示例中,超类是Inventory,子类是Books和Pictures.子类与超类形成部分/不完全不相交的联合.我将通过以下参考资料,因为这是一个有点复杂的主题,在这里描述.基本上,仔细设计和使用将其外键基于超类密钥的复合主键将处理(B),并且Inventory中不存在Type属性,从而处理(A).
因此,您的模式定义只需要进行一些小的更改,即删除Type属性,因为它是冗余的,并且需要触发器来保持数据对齐:
CREATE TABLE inventory (
id CHAR(1) PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE pictures (
inv_id CHAR(1) PRIMARY KEY REFERENCES inventory(id) ON UPDATE CASCADE ON DELETE CASCADE,
quality TEXT
);
CREATE TABLE books (
inv_id CHAR(1) PRIMARY KEY REFERENCES inventory(id) ON UPDATE CASCADE ON DELETE CASCADE,
author TEXT
);
现在,根据模式设计的本质,没有办法将库存项目视为“书籍”,但是没有书籍属性,因此不需要为此设置触发器.
如果要选择所有图片及其图片特定属性:
SELECT id, name, quality
FROM inventory, pictures
WHERE id = inv_id;
如果要选择所有图片或misc项目:
SELECT id, name
FROM inventory
WHERE id NOT IN (SELECT inv_id FROM books);
就要求(C)而言,从不允许对属性进行更新,这是一种奇怪的更新.这实际上可能最好使用权限或应用程序级别的某些东西来完成,因为我看不到您希望无法修复错误的情况.
了解更多信息:
> Good Blog Post
> Elmasri Chapter 8 Slides
> Elmasri Chapter 9 Slides- Start reading on slide 17