如何在PostgreSQL中维护与触发器的跨表一致性?

数据库表之间的某些相互依赖性无法(轻松)仅使用外键和检查约束进行建模.在我目前的项目中,我已经开始为所有这些条件编写约束触发器,但看起来如果我走这条路线,我将最终得到数百个触发器.

我的主要问题是:

>下面概述的场景中的触发器和约束是否实际覆盖了所有基础,或者是否仍然可以以结果不一致的方式添加/修改数据?
>写下所有这些触发器真的是正确的方法吗?我很少在第三方数据库模式中看到约束触发器.其他人是否只是相信应用程序不会搞砸?

最小的示例场景

中央“库存”表包含所有跟踪的项目.一些库存物品具有特定尺寸的特定类型;这些额外的尺寸存储在单独的表格中(“书籍”,“图片”).这个基本的表格布局不能改变(这只是一个例子;实际的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

点赞