我有一组元素。从数学意义上来说,我想用多种方式来划分集合。分区被定义为子集的集合,使得每个元素恰好位于一个子集中。
我想在关系数据库中表示这种情况,并且我希望关系约束来强制执行分区的数学定义。
我得到的最接近的是这样的模式:
CREATE TABLE element (
id integer PRIMARY KEY
);
CREATE TABLE partition (
id integer PRIMARY KEY
);
CREATE TABLE subset (
id integer PRIMARY KEY,
partition_id integer REFERENCES partition (id)
);
CREATE TABLE element_subset_mapping (
element_id integer REFERENCES element (id) NOT NULL,
partition_id integer REFERENCES partition (id) NOT NULL,
subset_id integer REFERENCES subset (id) NOT NULL,
CONSTRAINT uq_element_partition UNIQUE (element_id, partition_id)
);
这正确地表示一个元素只能出现在给定分区的一个子集中。但至少存在几个问题:
感觉应该有一个规范的方法来做到这一点,但我的搜索没有得到任何答案。
subset
上的主键约束已经强制执行了子集不属于多个分区的要求;但是,没有强制要求 subset_id
中的 partition_id
和 element_subset_mapping
的组合引用 subset
中的对应对。以下 DDL 通过使子集和分区的组合在 subset
中唯一并将 subset_id
上的外键和 partition_id
中的 element_subset_mapping
组合成引用 subset(id, partition_id)
的单个外键来解决这些问题:
CREATE TABLE element (
id integer PRIMARY KEY
);
CREATE TABLE partition (
id integer PRIMARY KEY
);
CREATE TABLE subset (
id integer PRIMARY KEY,
partition_id integer NOT NULL REFERENCES partition (id),
CONSTRAINT subset_partition_uq UNIQUE (id, partition_id)
);
CREATE TABLE element_subset_mapping (
element_id integer REFERENCES element (id) NOT NULL,
partition_id integer NOT NULL,
subset_id integer NOT NULL,
CONSTRAINT element_subset_mapping_partition_uq UNIQUE (element_id, partition_id),
CONSTRAINT element_subset_mapping_subset_fk FOREIGN KEY (subset_id, partition_id) REFERENCES subset(id, partition_id)
);
每个元素恰好属于每个分区中的一个子集的要求是一个更复杂的约束,因为它涉及来自多个表的行。让我们首先创建一个触发器函数,如果任何分区中缺少任何元素,该函数将引发异常:
CREATE OR REPLACE FUNCTION validate_all_elements_in_all_partitions() RETURNS trigger AS
$FUNC$
DECLARE
has_partition_with_missing_element BOOLEAN;
BEGIN
SELECT TRUE
INTO has_partition_with_missing_element
FROM element e
CROSS JOIN partition p
LEFT JOIN element_subset_mapping m
ON e.id = m.element_id
AND p.id = m.partition_id
WHERE m.subset_id IS NULL
LIMIT 1;
IF has_partition_with_missing_element THEN
RAISE EXCEPTION 'partition has missing element';
END IF;
RETURN NULL;
END;
$FUNC$ LANGUAGE plpgsql;
此函数可用于在每个可能导致违反约束的表上定义约束触发器:
element
、partition
和element_subset_mapping
。
DROP TRIGGER IF EXISTS all_elements_in_all_partitions ON element;
CREATE CONSTRAINT TRIGGER all_elements_in_all_partitions
AFTER DELETE OR INSERT OR UPDATE
ON element
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION validate_all_elements_in_all_partitions();
DROP TRIGGER IF EXISTS all_elements_in_all_partitions ON partition;
CREATE CONSTRAINT TRIGGER all_elements_in_all_partitions
AFTER DELETE OR INSERT OR UPDATE
ON partition
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION validate_all_elements_in_all_partitions();
DROP TRIGGER IF EXISTS all_elements_in_all_partitions ON element_subset_mapping;
CREATE CONSTRAINT TRIGGER all_elements_in_all_partitions
AFTER DELETE OR INSERT OR UPDATE
ON element_subset_mapping
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION validate_all_elements_in_all_partitions();
这些触发器的一个重要方面是它们是
DEFERRED
,这意味着它们在提交当前事务时触发,而不是在执行每行或语句时触发。这允许在事务内执行一组 SQL 命令以达到与关系要求一致的状态。