3

在我正在处理的项目中,我有一个需要定义为具有一对九关系的表,我想知道在数据库中创建它的最佳方法是什么?我在 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 个对另一个表的引用?存储过程?以编程方式?

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

4

5 回答 5

4

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;

}

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

于 2012-08-11T09:27:53.533 回答
1

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

如果我理解正确,您希望自动删除悬空指针。会有... REFERENCES other_table(id) ON DELETE CASCADE帮助吗?

于 2012-08-10T21:44:27.680 回答
0

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

因此,以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) 
);
于 2012-08-11T10:49:34.607 回答
0

我认为这不能通过约束来完成,请参阅如何编写关于 postgresql 中最大行数的约束?有几个想法。

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

  • foos 和 bar 是独立的实体,它们的生命周期不相互依赖
  • 关系存储在单独的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;
于 2012-08-11T08:26:08.467 回答
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)
);
于 2012-08-11T10:59:36.883 回答