约束一对多关系或使用预定义关系约束创建表

时间:2012-08-10 21:36:29

标签: sql database postgresql database-design database-schema

在我正在研究的项目中,我有一个表需要被定义为具有一到九个关系,我想知道在数据库中创建它的最佳方法是什么?我在PostgreSQL工作。

我最初的想法是创建一个表并且只是显式创建链接(请注意,actual_id是因为系统我必须虚拟化id,因为我需要唯一的表但我还需要知道模板的实际id是)

CREATE TABLE template (
    id int,
    actual_id int,
    foreign_key0 int references other_table(id),
    foreign_key1 int references other_table(id),
    foreign_key2 int references other_table(id),
    foreign_key3 int references other_table(id),
    foreign_key4 int references other_table(id),
    foreign_key5 int references other_table(id),
    foreign_key6 int references other_table(id),
    foreign_key7 int references other_table(id),
    foreign_key8 int references other_table(id)
);

然而,当我想要在没有引用任何内容时从引用的表中清除数据时,这会产生一个问题。另外我非常肯定这是从一开始就糟糕的数据库设计。

我的另一个想法是,我只想用一个约束

制作表格
CREATE TABLE template (
    id int,
    actual_id int,
    foreign_key0 int references other_table(id) );

但是这里的问题是如何将此限制为仅对另一个表有9个引用?存储过程?编程?

最终如果我坚持第一种方式,我很确定我只需要将所有不同的foreign_key选择到另一个只有一列的表中,并将其与other_table的id进行比较。我不想这样做。看起来真的很蠢。我真的想第二种方式,但我不知道如何最好地解决这个问题。

5 个答案:

答案 0 :(得分:3)

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”max更改为8或10,或者完全不插入任何子项,则COMMIT将失败,例如:

regress=# commit;
ERROR:  During INSERT: Parent id 5 must have exactly 9 children, not 8

如果您只要求每个家长拥有9个孩子的最多,而不是上述触发器中实现的完全 9个孩子,则可以删除{{1} },将DEFERRABLE INITIALLY DEFERRED更改为<> 9,并在<= 9触发器中删除DELETE处理程序。


顺便说一下,如果我在Java中使用JPA或其他一些相当聪明的ORM,我只会限制父级子集的大小:

child

方式更简单,尽管在数据库级别没有强制执行。

答案 1 :(得分:1)

  

但是当我想要在没有引用任何内容时从引用的表中清除数据时,这会产生一个问题。

如果我理解正确,你会想要悬挂指针自动删除。 ... REFERENCES other_table(id) ON DELETE CASCADE会有帮助吗?

答案 2 :(得分:0)

我不认为这可以通过约束来完成,请参阅How to write a constraint concerning a max number of rows in postgresql?了解几个想法。

下面我写了一个例子,其中foo保持与bar的关系计数,并做出以下假设:

  • foos和bars是独立实体,其生命周期不相互依赖
  • 关系存储在单独的foo2bar映射表
  • 必须首先从映射表中删除映射
  • 忽略没有关系的foos和bar的删除

\pset pager off

begin;

create table foo(id serial primary key, data text not null,
                 bar_count integer check(bar_count >= 0 and bar_count <= 3));

create table bar(id serial primary key, data text not null);

create table foo2bar(id serial primary key,
                     foo_id integer not null references foo(id),
                     bar_id integer not null references bar(id));

create or replace function trigger_update_bar_count() returns trigger
as $$
declare
  v_bar_count integer := 0;
begin
  if TG_OP = 'INSERT' then
    select count(*) into v_bar_count from foo2bar where foo_id = new.foo_id;

    update foo
       set bar_count = v_bar_count + 1
     where id = new.foo_id;

    return new;
   elsif TG_OP = 'DELETE' then
    select count(*) into v_bar_count from foo2bar where foo_id = old.foo_id;

    update foo
       set bar_count = v_bar_count - 1
     where id = old.foo_id;

    return old;
  end if;

end;
$$ language plpgsql;

create trigger trigger_foo2bar_1
before insert or delete on foo2bar
for each row execute procedure trigger_update_bar_count();

insert into foo(data) values('foo 1');

insert into bar(data) values('bar 1');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));

insert into bar(data) values('bar 2');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));
insert into bar(data) values('bar 3');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));

insert into foo(data) values('foo 2');

insert into bar(data) values('bar 4');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));
insert into bar(data) values('bar 5');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));
insert into bar(data) values('bar 6');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));

insert into foo(data) values('foo 3');

insert into bar(data) values('bar 7');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));
insert into bar(data) values('bar 8');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));
insert into bar(data) values('bar 9');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));

-- deletes only mappings
delete from foo2bar where foo_id = 1;
delete from foo2bar where bar_id = 6;

-- This will raise because the check constraint will be violated
-- insert into bar(data) values('bar 10');
-- insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
--                                            currval('bar_id_seq'));

select * from foo order by id;
select * from bar order by id;
select * from foo2bar order by id;

select foo.data as foo, bar.data as bar
  from foo2bar
 inner join foo on foo2bar.foo_id = foo.id
 inner join bar on foo2bar.bar_id = bar.id
 order by foo2bar.id
;

rollback;

答案 3 :(得分:0)

一个不同的想法。这是1对多关系(只有n限制为9),在1对多关系中,外键引用与您拥有的方式相反。

所以,反过来制定FOREIGN KEY约束(奖励:你只需要这样一个)并添加counter列和CHECK约束来限制相关数量行最多为9:

CREATE TABLE template (
    template_id int,
    actual_id int,
    PRIMARY KEY (template_id)
);

CREATE TABLE other_table (
    other_table_id int,
    template_id,
    counter smallint NOT NULL,
    --- other columns,
    PRIMARY KEY (other_table_id),
    UNIQUE KEY (template_id, counter),
    CHECK (counter BETWEEN 1 AND 9),
    FOREIGN KEY (template_id)
       REFERENCES template (template_id) 
);

答案 4 :(得分:0)

可维护和灵活的方法是规范化。而不只是这样做:

CREATE TABLE template (
    id int,
    actual_id int,
    foreign_key0 int references other_table(id),
    foreign_key1 int references other_table(id),
    foreign_key2 int references other_table(id),
    foreign_key3 int references other_table(id),
    foreign_key4 int references other_table(id),
    foreign_key5 int references other_table(id),
    foreign_key6 int references other_table(id),
    foreign_key7 int references other_table(id),
    foreign_key8 int references other_table(id)
);

以标准化方式执行,引入第三个表(template_ assoc _other_table):

CREATE TABLE template (
    id int not null primary key,
    actual_id int -- I don't what is this
    -- ...other fields here
);


create table template__assoc__other_table
(
    template_id int not null references template(id),
    other_table_id int not null references other_table(id),
    constraint pk_template__assoc__other_table 
        primary key (template_id, other_table_id)
);