数据库表之间的某些相互依赖性无法(轻松)仅使用外键和检查约束进行建模。在我目前的项目中,我已经开始为所有这些条件编写约束触发器,但是如果我走这条路,看起来我最终会得到数百个触发器。
我的主要问题是:
中央"库存"表包含所有跟踪的项目。一些库存物品具有特定尺寸的特定类型;这些额外的尺寸存储在单独的表格中("书籍","图片")。这个基本的表格布局不能改变(这只是一个例子;实际的DB显然有更多的表和列)。
额外要求:
(A)"库存中的每一行"表格类型为" book"必须在" books"中有匹配的行。 (同样适用于"图片")
(B)"书籍中的每一行" table必须指向" inventory"中的唯一行其类型是" book" (同样适用于"图片")
(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');
答案 0 :(得分:2)
Zilk,这里的问题是你首先要说的是架构无法修改。您需要编写此触发器spaghetti代码的原因是因为业务逻辑和架构的设计不一致。例如,trg_inv_details_delete
正在尖叫着我只是试图重新发明外键参照完整性 - 像Postgres这样的DBMS为你做的事情。
这实际上是一个非常典型的子类/超类层次结构问题,需要在数据库的设计阶段解决。您试图表达的实际迷你世界的细节将决定您如何对此进行建模。从那个知识将来enhanced ERD,然后你将这个概念模型转换为逻辑模式。
在此特定示例中,超类为Inventory
,子类为Books
和Pictures
。子类与超类形成部分/不完全不相交的联合。我将通过以下参考资料,因为这是一个有点复杂的主题,在这里描述。基本上,仔细设计和使用将其外键基于超类密钥的复合主键将处理(B),并且Type
中Inventory
属性的存在将是不必要的,从而照顾(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
);
现在,根据架构设计的性质,没有办法将库存项目视为"书籍,"例如,但还没有book属性,因此不需要触发它。
如果要选择所有照片及其特定于照片的属性:
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)而言,从不允许对属性进行更新,这是一种奇怪的属性。实际上,这可能最好使用权限或应用程序级别的某些内容来完成,因为我无法看到您希望无法修复错误的情况。
详情请见: