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

时间:2014-11-28 15:23:26

标签: sql postgresql database-design triggers

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

我的主要问题是:

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

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

1 个答案:

答案 0 :(得分:2)

Zilk,这里的问题是你首先要说的是架构无法修改。您需要编写此触发器spaghetti代码的原因是因为业务逻辑和架构的设计不一致。例如,trg_inv_details_delete正在尖叫着我只是试图重新发明外键参照完整性 - 像Postgres这样的DBMS为你做的事情。

这实际上是一个非常典型的子类/超类层次结构问题,需要在数据库的设计阶段解决。您试图表达的实际迷你世界的细节将决定您如何对此进行建模。从那个知识​​将来enhanced ERD,然后你将这个概念模型转换为逻辑模式。

在此特定示例中,超类为Inventory,子类为BooksPictures。子类与超类形成部分/不完全不相交的联合。我将通过以下参考资料,因为这是一个有点复杂的主题,在这里描述。基本上,仔细设计和使用将其外键基于超类密钥的复合主键将处理(B),并且TypeInventory属性的存在将是不必要的,从而照顾(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)而言,从不允许对属性进行更新,这是一种奇怪的属性。实际上,这可能最好使用权限或应用程序级别的某些内容来完成,因为我无法看到您希望无法修复错误的情况。

详情请见: