1:n 关系总是可以反转为 n:1 。换句话说,而不是:
parent:field1 -> child1:id
parent:field2 -> child2:id
parent:field3 -> child3:id
....
parent:field9 -> child9
你总是可以写:
child1:parent_id -> parent:id
child2:parent_id -> parent:id
child3:parent_id -> parent:id
....
child9:parent_id -> parent:id
...并通过触发器或在应用程序中限制每个父母的孩子数量。这是我强烈推荐的方法。您需要一个可延迟的约束触发器来允许您插入任何内容。
如果要在数据库中强制执行,请使用约束触发器。给定虚拟模式:
CREATE TABLE parent (id serial primary key);
CREATE TABLE child( id serial primary key, parent_id integer references parent(id) );
INSERT INTO parent (id) values ( DEFAULT );
INSERT INTO child ( parent_id )
SELECT p.id FROM parent p CROSS JOIN generate_series(1,9) x;
你可以写:
CREATE OR REPLACE FUNCTION children_per_parent() RETURNS TRIGGER AS $$
DECLARE
n integer;
BEGIN
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
SELECT INTO n count(id) FROM child WHERE parent_id = NEW.parent_id;
IF n <> 9 THEN
RAISE EXCEPTION 'During % of child: Parent id=% must have exactly 9 children, not %',tg_op,NEW.parent_id,n;
END IF;
END IF;
IF TG_OP = 'UPDATE' OR TG_OP = 'DELETE' THEN
SELECT INTO n count(id) FROM child WHERE parent_id = OLD.parent_id;
IF n <> 9 THEN
RAISE EXCEPTION 'During % of child: Parent id=% must have exactly 9 children, not %',tg_op,NEW.parent_id,n;
END IF;
END IF;
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';
CREATE CONSTRAINT TRIGGER children_per_parent_tg
AFTER INSERT OR UPDATE OR DELETE ON child
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE children_per_parent();
CREATE OR REPLACE parent_constrain_children() RETURNS trigger AS $$
DECLARE
n integer;
BEGIN
IF TG_OP = 'INSERT' THEN
SELECT INTO n count(id) FROM child WHERE parent_id = NEW.id;
IF n <> 9 THEN
RAISE EXCEPTION 'During INSERT of parent id=%: Must have 9 children, found %',NEW.id,n;
END IF;
END IF;
-- No need for an UPDATE or DELETE check, as regular referential integrity constraints
-- and the trigger on `child' will do the job.
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';
CREATE CONSTRAINT TRIGGER parent_limit_children_tg
AFTER INSERT ON parent
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE parent_constrain_children();
请注意,上面有两个触发器。对孩子的触发是显而易见的。需要在父级上触发以防止插入没有任何子级的父级。
现在观察一个测试:
regress=# delete from child;
ERROR: During DELETE: Parent id 1 must have exactly 9 children, not 0
regress=# insert into child( parent_id) SELECT id FROM parent;
ERROR: During INSERT: Parent id 1 must have exactly 9 children, not 10
因为延迟约束触发器是在事务提交时检查的,而不是立即或在语句结束时检查,所以您仍然可以这样做:
regress# BEGIN;
BEGIN
regress# INSERT INTO parent (id) values ( DEFAULT ) RETURNING id;
id
----
2
INSERT 0 1
regress# insert into child ( parent_id ) SELECT p.id FROM parent p CROSS JOIN generate_series(1,9) x WHERE p.id = 4;
INSERT 0 9
regress# COMMIT;
COMMIT
...但是如果您将“generate_series”最大值更改为 8 或 10,或者完全不插入任何子项,COMMIT 将失败,例如:
regress=# commit;
ERROR: During INSERT: Parent id 5 must have exactly 9 children, not 8
如果您只要求每个父级最多有 9 个子级,而不是上述触发器中实现的 9 个子级,则可以删除DEFERRABLE INITIALLY DEFERRED
,将 更改<> 9
为<= 9
,然后删除触发器DELETE
中的处理程序child
。
顺便说一句,如果我在 Java 或其他一些相当聪明的 ORM 中使用 JPA,我只会限制父级的子级集合的大小:
@Entity
public Parent {
@Column
@Size(min=9,max=9)
private Collection<Child> collectionOfChildren;
}
方式更简单,尽管没有在数据库级别强制执行。