Sich gegenseitig ausschließende Viele-zu-Viele-Beziehungen

9

Ich habe eine Tabelle , containersdie eine many-to-many - Beziehungen zu mehreren Tabellen haben, lassen Sie uns sagen , die sind plants, animalsund bacteria. Jeder Behälter kann eine beliebige Anzahl von Pflanzen, Tieren oder Bakterien enthalten, und jede Pflanze, jedes Tier oder jedes Bakterium kann sich in einer beliebigen Anzahl von Behältern befinden.

Bisher ist dies sehr einfach, aber der Teil, mit dem ich ein Problem habe, ist, dass jeder Container nur Elemente des gleichen Typs enthalten sollte. Gemischte Behälter, die z. B. sowohl Pflanzen als auch Tiere enthalten, sollten eine Einschränkung der Datenbank darstellen.

Mein ursprüngliches Schema hierfür war das Folgende:

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


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


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


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

Mit diesem Schema kann ich mir jedoch nicht vorstellen, wie die Einschränkung implementiert werden soll, dass Container homogen sein sollen.

Gibt es eine Möglichkeit, dies mit referenzieller Integrität zu implementieren und auf Datenbankebene sicherzustellen, dass die Container homogen sind?

Ich benutze dafür Postgres 9.6.

Verrückter Wissenschaftler
quelle
1
Sind die Behälter homogen? Das heißt, kann ein Behälter, in dem sich heute Pflanzen befinden, entleert werden und morgen ohne Änderungen Tiere oder Bakterien aufnehmen?
RDFozz
@RDFozz Ich habe nicht vor, das in der Benutzeroberfläche zuzulassen, aber im Prinzip wäre es möglich. Es ist nicht wirklich sinnvoll, dies zu tun. Das Löschen des Containers und das Erstellen eines neuen Containers wäre die typische Aktion. Aber wenn ein Container die Art des Inhalts ändern würde, würde er nichts kaputt machen
Mad Scientist

Antworten:

10

Es gibt eine Möglichkeit, dies nur deklarativ zu implementieren, ohne Ihr aktuelles Setup wesentlich zu ändern, wenn Sie damit einverstanden sind, eine gewisse Redundanz einzuführen. Was folgt, kann als eine Entwicklung auf RDFozz 'Vorschlag betrachtet werden , obwohl die Idee in meinem Kopf vollständig entstanden ist, bevor ich seine Antwort gelesen habe (und es ist anders genug, um einen eigenen Antwortbeitrag zu rechtfertigen).

Implementierung

Hier ist, was Sie Schritt für Schritt tun:

  1. Erstellen Sie eine containerTypesTabelle nach dem Vorbild der Antwort von RDFozz:

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

    Füllen Sie es mit vordefinierten IDs für jeden Typ. Lassen Sie sie für diese Antwort mit dem Beispiel von RDFozz übereinstimmen: 1 für Pflanzen, 2 für Tiere, 3 für Bakterien.

  2. Fügen Sie eine containerType_idSpalte hinzu containersund machen Sie sie nicht nullbar und einen Fremdschlüssel.

    ALTER TABLE containers
    ADD containerType_id int NOT NULL
      REFERENCES containerTypes (id);
  3. Angenommen, die idSpalte ist bereits der Primärschlüssel von containers, erstellen Sie eine eindeutige Einschränkung für (id, containerType_id).

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

    Hier beginnen die Redundanzen. Wenn ides als Primärschlüssel deklariert wird, können wir sicher sein, dass es eindeutig ist. Wenn es eindeutig ist, muss jede Kombination aus idund einer anderen Spalte auch ohne zusätzliche Erklärung der Eindeutigkeit eindeutig sein. Worum geht es also? Der Punkt ist, dass wir durch formelles Deklarieren des Spaltenpaars als eindeutig verweisbar machen , dh das Ziel einer Fremdschlüsseleinschränkung sein, worum es in diesem Teil geht.

  4. Fügen Sie eine containerType_idSpalte zu jedem der Kreuzung Tabellen ( containers_animals, containers_plants, containers_bacteria). Das Erstellen eines Fremdschlüssels ist völlig optional. Entscheidend ist, dass die Spalte für alle Zeilen den gleichen Wert hat, der für jede Tabelle unterschiedlich ist: 1 für containers_plants, 2 für containers_animals, 3 für containers_bacteria, gemäß den Beschreibungen in containerTypes. In jedem Fall können Sie diesen Wert auch als Standard festlegen, um Ihre Einfügeanweisungen zu vereinfachen:

    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. Machen Sie in jeder der Junction-Tabellen das Spaltenpaar zu (container_id, containerType_id)einer Referenz für Fremdschlüsseleinschränkungen 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);

    Wenn container_idbereits ein Verweis auf definiert ist, können Sie containersdiese Einschränkung aus jeder Tabelle entfernen, wenn dies nicht mehr erforderlich ist.

Wie es funktioniert

Indem Sie die Spalte Containertyp hinzufügen und an den Fremdschlüsseleinschränkungen teilnehmen lassen, bereiten Sie einen Mechanismus vor, der verhindert, dass sich der Containertyp ändert. Das Ändern des Typs im containersTyp wäre nur möglich, wenn die Fremdschlüssel mit der DEFERRABLEKlausel definiert würden , die in dieser Implementierung nicht enthalten sein soll.

Selbst wenn sie aufschiebbar wären, wäre eine Änderung des Typs aufgrund der containersPrüfbeschränkung auf der anderen Seite der Beziehung der Verbindungstabelle nicht möglich. Jede Kreuzungstabelle erlaubt nur einen bestimmten Containertyp. Das verhindert nicht nur vorhandene Referenzen aus dem Typ zu ändern , sondern auch verhindert , dass zusätzlich die falschen Referenzen. Das heißt, wenn Sie einen Container vom Typ 2 (Tiere) haben, können Sie ihm nur Elemente hinzufügen, indem Sie die Tabelle verwenden, in der Typ 2 zulässig ist containers_animals, dh keine Zeilen hinzufügen kann, die darauf verweisen, z. B. containers_bacteriawas akzeptiert Nur Behälter vom Typ 3.

Schließlich macht es Ihre eigene Entscheidung, unterschiedliche Tabellen für plants, animalsund bacteriaund unterschiedliche Junction-Tabellen für jeden Entitätstyp zu haben, einem Container bereits unmöglich, Elemente von mehr als einem Typ zu haben.

Alle diese Faktoren zusammen sorgen also auf rein deklarative Weise dafür, dass alle Ihre Container homogen sind.

Andriy M.
quelle
3

Eine Option ist das Hinzufügen eines containertype_idzur ContainerTabelle. Machen Sie die Spalte NICHT NULL und einen Fremdschlüssel für eine ContainerTypeTabelle, die Einträge für jeden Elementtyp enthält, der in einen Container aufgenommen werden kann:

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

Um sicherzustellen, dass der Containertyp nicht geändert werden kann, erstellen Sie einen Aktualisierungsauslöser, der prüft, ob der containertype_idaktualisiert wurde, und die Änderung in diesem Fall rückgängig macht.

Überprüfen Sie dann beim Einfügen und Aktualisieren von Triggern für Ihre Containerverknüpfungstabellen die Datei includeertype_id mit dem Entitätstyp in dieser Tabelle, um sicherzustellen, dass sie übereinstimmen.

Wenn alles, was Sie in einen Container einfügen, mit dem Typ übereinstimmen muss und der Typ nicht geändert werden kann, ist alles im Container vom selben Typ.

HINWEIS: Da der Auslöser in den Verknüpfungstabellen entscheidet, welche Übereinstimmungen vorliegen, können Sie diesen Typ erstellen, dem Container zuordnen und dies überprüfen, wenn Sie einen Containertyp benötigen, der Pflanzen und Tiere enthalten kann . Sie behalten also Ihre Flexibilität, wenn sich irgendwann etwas ändert (sagen wir, Sie erhalten die Typen "Zeitschriften" und "Bücher" ...).

HINWEIS: Wenn das meiste, was mit Containern passiert, dasselbe ist, unabhängig davon, was sich in ihnen befindet, ist dies sinnvoll. Wenn Sie sehr unterschiedliche Dinge haben (im System, nicht in unserer physischen Realität), basierend auf dem Inhalt des Containers, dann ist Evan Carrolls Idee, separate Tabellen für die separaten Containertypen zu haben, durchaus sinnvoll. Diese Lösung stellt fest, dass Container beim Erstellen unterschiedliche Typen haben, diese jedoch in derselben Tabelle belassen. Wenn Sie den Typ jedes Mal überprüfen müssen, wenn Sie eine Aktion für einen Container ausführen, und wenn die Aktion, die Sie ausführen, vom Typ abhängt, sind separate Tabellen möglicherweise schneller und einfacher.

RDFozz
quelle
Es ist eine Möglichkeit, dies zu tun, aber es gibt viele Nachteile: Dazu sind drei Index-Scans erforderlich, um die Liste der Container / Anlagen wieder zusammenzusetzen. Es verlangsamt das Einfügen durch Hinzufügen einer Auswahl in einer fremden Tabelle und reduziert die Integrität auf eine Funktion von Trigger - manchmal funktioniert das, aber ich würde es nie wünschen, es verlangsamt auch Aktualisierungen, um sicherzustellen, dass die Spalte nicht geändert wird. Alles in allem denke ich, dass wir uns mehr um mentale Blockaden kümmern, als um die Anforderungen einer App zu erfüllen, aber nach den Abstimmungen bin ich vielleicht allein.
Evan Carroll
1
Wir wissen nicht genau, was von hier aus passieren muss. Wenn sich der Großteil der Anwendung auf die Container selbst konzentriert (Versand, Verfolgung, Lokalisierung in Lagereinrichtungen usw.), konzentrieren sich die meisten Abfragen möglicherweise nicht auf den Inhalt der Container, sondern nur auf die Container selbst. Wie ich bereits erwähnt habe, gibt es definitiv Szenarien, in denen die Behandlung eines Pflanzenbehälters als eine völlig andere Einheit als ein Tierbehälter sinnvoll ist. OP muss entscheiden, mit welchem ​​Szenario sie konfrontiert sind.
RDFozz
3

Wenn Sie nur 2 oder 3 Kategorien (Pflanzen / Metazoen / Bakterien) benötigen und eine XOR-Beziehung modellieren möchten, ist möglicherweise ein "Bogen" die Lösung für Sie. Vorteil: keine Trigger erforderlich. Beispieldiagramme finden Sie [hier] [1]. In Ihrer Situation würde die Tabelle "Container" 3 Spalten mit einer CHECK-Einschränkung enthalten, die entweder eine Pflanze, ein Tier oder ein Bakterium zulassen.

Dies ist wahrscheinlich nicht angemessen, wenn in Zukunft zwischen vielen Kategorien (z. B. Gattungen, Arten, Unterarten) unterschieden werden muss. Für 2-3 Gruppen / Kategorien kann dies jedoch den Trick tun.

UPDATE: Inspiriert von den Vorschlägen und Kommentaren des Mitwirkenden, einer anderen Lösung, die viele Taxa (Gruppen verwandter Organismen, vom Biologen klassifiziert) zulässt und "spezifische" Tabellennamen vermeidet (PostgreSQL 9.5).

DDL-Code:

-- 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) ;

Testdaten:

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');

Testen:

-- 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');

Vielen Dank an @RDFozz und @Evan Carroll und @ypercube für ihren Input und ihre Geduld (Lesen / Korrigieren meiner Antworten).

stefan
quelle
1

Erstens stimme ich @RDFozz beim Lesen der Frage zu. Er äußert jedoch einige Bedenken hinsichtlich der Antwort von Stefans .

Geben Sie hier die Bildbeschreibung ein

Um seine Bedenken auszuräumen, nur

  1. Entferne das PRIMARY KEY
  2. Fügen Sie die UNIQUEEinschränkungen zum Schutz vor doppelten Einträgen hinzu.
  3. Fügen Sie EXCLUSIONEinschränkungen hinzu, um sicherzustellen, dass die Container "homogen" sind.
  4. Fügen Sie einen Index hinzu c_id, um eine angemessene Leistung sicherzustellen.
  5. Töte jeden, der dies tut, und zeige ihn auf meine andere Antwort auf Vernunft.

So sieht es aus:

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);

Jetzt können Sie einen Container mit mehreren Dingen haben, aber nur einen Typ in einem Container.

# 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).

Und alles ist in GIST-Indizes implementiert.

Die Große Pyramide von Gizeh hat nichts mit PostgreSQL zu tun.

Evan Carroll
quelle
0

Ich habe einen Tischbehälter, der viele zu viele Beziehungen zu mehreren Tischen haben kann, sagen wir, das sind Pflanzen, Tiere und Bakterien.

Das ist eine schlechte Idee.

Mit diesem Schema kann ich mir jedoch nicht vorstellen, wie die Einschränkung implementiert werden soll, dass Container homogen sein sollen.

Und jetzt weißt du warum. =)

Ich glaube, Sie bleiben bei der Idee der Vererbung durch objektorientierte Programmierung (OO). OO-Vererbung löst ein Problem bei der Wiederverwendung von Code. In SQL ist redundanter Code das geringste unserer Probleme. Integrität ist in erster Linie. Leistung ist oft an zweiter Stelle. Wir werden die ersten beiden Schmerzen haben. Wir haben keine "Kompilierungszeit", die die Kosten eliminieren kann.

Verzichten Sie also einfach auf Ihre Besessenheit, Code wiederzuverwenden. Behälter für Pflanzen, Tiere und Bakterien unterscheiden sich überall in der realen Welt grundlegend. Die Code-Wiederverwendungskomponente von "hold stuff" erledigt das einfach nicht für Sie. Zerbrich sie. Dies bringt Ihnen nicht nur mehr Integrität und Leistung, sondern in Zukunft fällt es Ihnen auch leichter, Ihr Schema zu erweitern: Schließlich mussten Sie in Ihrem Schema bereits die enthaltenen Elemente (Pflanzen, Tiere usw.) aufteilen. , scheint zumindest möglich, dass Sie die Behälter auseinander brechen müssen. Sie werden dann nicht Ihr gesamtes Schema neu gestalten wollen.

Evan Carroll
quelle
Das Aufteilen der Container würde das Problem auf einen anderen Teil des Schemas verschieben. Ich muss immer noch auf die Container aus anderen Tabellen verweisen, und diese Teile müssten auch die verschiedenen Containertypen unterscheiden.
Mad Scientist
Sie würden wissen, welchen Containertyp sie haben, nur an der Tabelle, in der sie den Container finden. Ich bin verwirrt darüber, was Sie meinen? Pflanzen verweisen auf einen einzelnen Behälter in plant_containersund so weiter. Dinge, die nur einen Pflanzenbehälter benötigen, werden nur aus der plant_containersTabelle ausgewählt. Dinge, die einen Container benötigen (dh alle Arten von Containern durchsuchen), können UNION ALLfür alle drei Tabellen mit Containern ausgeführt werden.
Evan Carroll