互斥的多对多关系


9

我有一个表containers,可以有几个表一个多一对多的关系,让我们说那些是plantsanimalsbacteria。每个容器可以包含任意数量的植物,动物或细菌,并且每个植物,动物或细菌可以位于任意数量的容器中。

到目前为止,这非常简单,但是我遇到的问题是每个容器应仅包含相同类型的元素。例如包含植物和动物的混合容器应成为数据库中的约束违例。

我的原始模式如下:

containers
----------
id
...
...


containers_plants
-----------------
container_id
plant_id


containers_animals
------------------
container_id
animal_id


containers_bacteria
-------------------
container_id
bacterium_id

但是使用这种模式,我无法提出如何实现容器应该是同质的约束。

有没有一种方法可以使用参照完整性来实现这一点,并确保在数据库级别上容器是同质的?

我为此使用Postgres 9.6。


1
容器是否均质?就是说,可以将今天盛放植物的容器倒空,明天不作任何改动就可以盛放动物或细菌吗?
RDFozz

@RDFozz我没有计划在用户界面中允许这样做,但是原则上是可能的。这样做没有任何意义,删除容器并创建一个新容器是典型的操作。但是,如果容器更改了内容的类型,则不会破坏任何内容
Mad Scientist

Answers:


10

如果您同意对其进行一些冗余,则只能以声明方式实现此方法,而无需大量更改当前设置。尽管我在阅读他的答案之前就已经完全形成了这个想法,但随后的内容可以认为是RDFozz的建议的发展(无论如何,该想法足以保证自己的答案)。

实作

这是您的操作,逐步进行:

  1. containerTypes按照RDFozz的答案中的建议创建一个表:

    CREATE TABLE containerTypes
    (
      id int PRIMARY KEY,
      description varchar(30)
    );

    使用每种类型的预定义ID填充它。出于此答案的目的,让它们与RDFozz的示例匹配:1用于植物,2用于动物,3用于细菌。

  2. 向添加一个containerType_idcontainers并使其不可为空,并添加一个外键。

    ALTER TABLE containers
    ADD containerType_id int NOT NULL
      REFERENCES containerTypes (id);
  3. 假设该id列已经是的主键containers,则对创建一个唯一约束(id, containerType_id)

    ALTER TABLE containers
    ADD CONSTRAINT UQ_containers_id_containerTypeId
      UNIQUE (id, containerType_id);

    这是冗余开始的地方。如果id声明为主键,我们可以放心它是唯一的。如果它是唯一的,id则没有其他声明唯一性的情况下,和的任何组合也必定是唯一的-那么,这有什么用呢?关键是,通过正式声明列对是唯一的,我们使它们成为可引用的,即成为外键约束的目标,这就是本部分的目的。

  4. 一添加containerType_id列各接合表(containers_animalscontainers_plantscontainers_bacteria)。使其成为外键是完全可选的。至关重要的是确保该列的所有行的值都相同,而每个表的值都不同:根据中的描述,1表示containers_plants,2表示containers_animals,3表示。在每种情况下,您都可以将该值设为默认值以简化您的插入语句:containers_bacteriacontainerTypes

    ALTER TABLE containers_plants
    ADD containerType_id NOT NULL
      DEFAULT (1)
      CHECK (containerType_id = 1);
    
    ALTER TABLE containers_animals
    ADD containerType_id NOT NULL
      DEFAULT (2)
      CHECK (containerType_id = 2);
    
    ALTER TABLE containers_bacteria
    ADD containerType_id NOT NULL
      DEFAULT (3)
      CHECK (containerType_id = 3);
  5. 在每个联结表中,使成对的列(container_id, containerType_id)成为外键约束引用containers

    ALTER TABLE containers_plants
    ADD CONSTRAINT FK_containersPlants_containers
      FOREIGN KEY (container_id, containerType_id)
      REFERENCES containers (id, containerType_id);
    
    ALTER TABLE containers_animals
    ADD CONSTRAINT FK_containersAnimals_containers
      FOREIGN KEY (container_id, containerType_id)
      REFERENCES containers (id, containerType_id);
    
    ALTER TABLE containers_bacteria
    ADD CONSTRAINT FK_containersBacteria_containers
      FOREIGN KEY (container_id, containerType_id)
      REFERENCES containers (id, containerType_id);

    如果container_id已经定义为对的引用containers,请随时从不再需要的表中删除该约束。

怎么运行的

通过添加容器类型列并使之参与外键约束,您可以准备一种防止容器类型更改的机制。containers仅在使用DEFERRABLE子句定义了外键的情况下,才可以在类型中更改类型,而在本实现中不应使用外键。

即使它们是可延迟的,由于— containers连接表关系的另一侧的检查约束,更改类型仍然是不可能的。每个联结表仅允许一种特定的容器类型。这不仅可以防止现有引用更改类型,还可以防止添加错误的类型引用。也就是说,如果您有一个类型2(动物)的容器,则只能使用允许使用类型2的表(即)向其中添加项目containers_animals,并且将无法添加引用它的行(例如)containers_bacteria,该行接受仅类型3的容器。

最后,您自己决定为plantsanimalsbacteria和设置不同的表,并为每种实体类型使用不同的联结表,这已经使容器不可能具有多个类型的项目。

因此,所有这些因素结合在一起,以纯粹的声明方式确保了所有容器都是同质的。


3

一种选择是将a添加containertype_idContainer表中。将列设置为NOT NULL,并创建ContainerType表的外键,该表将为容器中可以进入的每种类型的项提供条目:

containertype_id |   type
-----------------+-----------
        1        | plant
        2        | animal
        3        | bacteria

为了确保不能更改容器类型,请创建一个更新触发器,以检查容器是否containertype_id已更新,并在这种情况下回滚更改。

然后,在容器链接表上的插入和更新触发器中,对照该表中的实体类型检查containertype_id,以确保它们匹配。

如果您放入容器中的任何内容都必须与该类型匹配,并且该类型无法更改,则容器中的所有内容都将是同一类型。

注意:由于链接表上的触发器是决定什么匹配项的触发器,因此,如果您需要一种容器中可能包含动植物的类型,则可以创建该类型,将其分配给该容器,然后进行检查。因此,如果事情在某些时候有所变化,您将保留灵活性(例如,您获得的类型为“杂志”和“书” ...)。

注意第二点:如果容器中发生的大部分事情都是相同的,无论它们中包含什么,这都是有道理的。如果根据容器的内容发生的事情非常不同(在系统中,而不是在我们的物理现实中),那么Evan Carroll的想法就是为不同的容器类型创建单独的表,这是很有意义的。此解决方案确定容器在创建时具有不同的类型,但将它们保留在同一表中。如果您每次对容器执行操作时都必须检查类型,并且所执行的操作取决于类型,那么实际上单独的表可能会更快,更轻松。


这是一种方法,但是有很多缺点:这样做需要进行三个索引扫描以重新组装容器/植物的列表,通过在外部表中添加选择来减慢插入速度,将完整性降低为触发器-有时行得通,但我永远也不想这样做,它还会减慢更新速度,以确保未修改列。所有这些都表明,我认为我们在解决心理障碍方面的工作比满足应用程序的需求还要多,但是从投票的角度来看,我可能是一个人。
埃文·卡罗尔

1
我们不确切地知道从这里发生什么。如果应用程序的大部分内容集中在容器本身(运输,跟踪它们,将它们放置在存储设施中等),则大多数查询可能不会只关注容器本身,而只关注它们的内容。正如我所指出的,在某些情况下,将植物容器视为与动物容器完全不同的实体绝对是有意义的。OP必须决定他们面对的情况。
RDFozz

3

如果您只需要2或3个类别(植物/ metazoa /细菌),并且要对XOR关系建模,那么“弧形”可能是您的解决方案。优势:无需触发器。示例图可在[此处] [1]中找到。在您的情况下,“容器”表将具有3列并带有CHECK约束,从而允许动植物或细菌。

如果将来需要区分很多类别(例如属,种,亚种),这可能不合适。但是,对于2-3个组/类别,这可以解决问题。

更新:受贡献者的建议和评论启发,提供了一种不同的解决方案,该解决方案允许许多分类群(按生物学家分类的相关生物群),并避免使用“特定的”表名(PostgreSQL 9.5)。

DDL代码:

-- containers: may have more columns eg for temperature, humidity etc
create table containers ( 
  ctr_name varchar(64) unique
);

-- taxonomy - have as many taxa as needed (not just plants/animals/bacteria)
create table taxa ( 
  t_name varchar(64) unique
);

create table organisms (
  o_id integer primary key
, o_name varchar(64)
, t_name varchar(64) references taxa(t_name)
, unique (o_id, t_name) 
);

-- table for mapping containers to organisms and (their) taxon, 
-- each container contains organisms of one and the same taxon
create table collection ( 
  ctr_name varchar(64) references containers(ctr_name)
, o_id integer 
, t_name varchar(64) 
, unique (ctr_name, o_id)
);

--  exclude : taxa that are different from those already in a container
alter table collection
add exclude using gist (ctr_name with =, t_name with <>);

--  FK : is the o_id <-> t_name (organism-taxon) mapping correct?
alter table collection
add constraint taxon_fkey
foreign key (o_id, t_name) references organisms (o_id, t_name) ;

测试数据:

insert into containers values ('container_a'),('container_b'),('container_c');
insert into taxa values('t:plant'),('t:animal'),('t:bacterium');
insert into organisms values 
(1, 'p1', 't:plant'),(2, 'p2', 't:plant'),(3, 'p3', 't:plant'),
(11, 'a1', 't:animal'),(22, 'a1', 't:animal'),(33, 'a1', 't:animal'),
(111, 'b1', 't:bacterium'),(222, 'b1', 't:bacterium'),(333, 'b1', 't:bacterium');

测试:

-- several plants can be in one and the same container (3 inserts succeed)
insert into collection values ('container_a', 1, 't:plant');
insert into collection values ('container_a', 2, 't:plant');
insert into collection values ('container_a', 3, 't:plant');
-- 3 inserts that fail:
-- organism id in a container must be UNIQUE
insert into collection values ('container_a', 1, 't:plant');
-- bacteria not allowed in container_a, populated by plants (EXCLUSION at work)
insert into collection values ('container_a', 333, 't:bacterium');
-- organism with id 333 is NOT a plant -> insert prevented by FK
insert into collection values ('container_a', 333, 't:plant');

感谢@RDFozz和@Evan Carroll和@ypercube的投入和耐心(阅读/更正了我的答案)。


1

首先,在阅读问题时,我同意@RDFozz。但是他对Stefans的回答提出了一些担忧,

在此处输入图片说明

为了解决他的担忧,

  1. 去除 PRIMARY KEY
  2. 添加UNIQUE约束以防止重复条目。
  3. 添加EXCLUSION约束以确保容器“同质”
  4. 添加索引c_id以确保良好的性能。
  5. 杀死任何这样做的人,将他们指向我的其他答案以理智。

看起来像这样

CREATE TABLE container ( 
  c_id int NOT NULL,
  p_id int,
  b_id int,
  a_id int,
  UNIQUE (c_id,p_id),
  UNIQUE (c_id,b_id),
  UNIQUE (c_id,a_id),
  EXCLUDE USING gist(c_id WITH =, (CASE WHEN p_id>0 THEN 1 ELSE 0 END) WITH <>),
  EXCLUDE USING gist(c_id WITH =, (CASE WHEN b_id>0 THEN 1 ELSE 0 END) WITH <>),
  EXCLUDE USING gist(c_id WITH =, (CASE WHEN a_id>0 THEN 1 ELSE 0 END) WITH <>),
  CHECK (
    ( p_id IS NOT NULL and b_id IS NULL and a_id IS NULL ) 
    OR ( p_id IS NULL and b_id IS NOT NULL and a_id IS NULL ) 
    OR ( p_id IS NULL and b_id IS NULL and a_id IS NOT NULL ) 
  )
);
CREATE INDEX ON container (c_id);

现在你可以有多个事情一个容器,但只有一个类型的容器中的事情。

# INSERT INTO container (c_id,p_id,b_id) VALUES (1,1,null);
INSERT 0 1
# INSERT INTO container (c_id,p_id,b_id) VALUES (1,null,2);
ERROR:  conflicting key value violates exclusion constraint "container_c_id_case_excl"
DETAIL:  Key (c_id, (
CASE
    WHEN p_id > 0 THEN 1
    ELSE 0
END))=(1, 0) conflicts with existing key (c_id, (
CASE
    WHEN p_id > 0 THEN 1
    ELSE 0
END))=(1, 1).

所有这些都在GIST索引上实现。

吉萨大金字塔在PostgreSQL上没有。


0

我有一个表容器,可以与多个表建立多对多关系,比方说它们是植物,动物和细菌。

那是个坏主意。

但是使用这种模式,我无法提出如何实现容器应该是同质的约束。

现在你知道为什么了。=)

我相信您一直坚持从面向对象编程(OO)继承的想法。OO继承解决了代码重用的问题。在SQL中,冗余代码是我们最少遇到的问题。诚信至上。性能通常是第二位。我们会为前两个感到痛苦。我们没有可以消除成本的“编译时间”。

因此,只需放弃对代码重用的痴迷。在现实世界中的每个地方,用于植物,动物和细菌的容器都有根本的不同。“持有东西”的代码重用组件只是为您做不到。分开。它不仅会为您提供更多的完整性和性能,而且在将来,您会发现扩展架构更加容易:毕竟,在架构中,您已经必须分解包含的项目(植物,动物等) ,看来至少有可能必须拆开容器。然后,您将不需要重新设计整个架构。


拆分容器会将问题移到架构的其他部分,我仍然需要从其他表中引用容器,而这些部分也必须区分不同的容器类型。
疯狂的科学家,

他们会只在找到容器的表上知道他们所拥有的容器类型。我对您的意思感到困惑?植物在中引用单个容器plant_containers,依此类推。仅需要植物容器的事物仅从plant_containers表中选择。需要任何容器的内容(即搜索所有类型的容器)可以UNION ALL在具有容器的所有三个表上执行。
埃文·卡罗尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.