Rapporti molti-a-molti reciprocamente esclusivi


9

Ho una tabella containersche può avere relazioni molte-a-molte con diverse tabelle, diciamo che sono plants, animalse bacteria. Ogni contenitore può contenere un numero arbitrario di piante, animali o batteri e ogni pianta, animale o batterio può trovarsi in un numero arbitrario di contenitori.

Finora questo è molto semplice, ma la parte con cui ho problemi è che ogni contenitore dovrebbe contenere solo elementi dello stesso tipo. Contenitori misti che contengono ad esempio piante e animali dovrebbero costituire una violazione del vincolo nel database.

Il mio schema originale per questo era il seguente:

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


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


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


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

Ma con questo schema, non riesco a trovare il modo di implementare il vincolo che i contenitori dovrebbero essere omogenei.

Esiste un modo per implementarlo con integrità referenziale e garantire a livello di database che i contenitori siano omogenei?

Sto usando Postgres 9.6 per questo.


1
I contenitori sono omogenei? Vale a dire, un contenitore che contiene piante oggi può essere svuotato e, senza cambiamenti, può contenere animali o batteri domani?
RDFozz,

@RDFozz Non ho intenzione di permetterlo nell'interfaccia utente, ma in linea di principio sarebbe possibile. Non ha davvero senso farlo, eliminare il contenitore e crearne uno nuovo sarebbe l'azione tipica. Ma se un contenitore cambiasse il tipo di contenuto, non spezzerebbe nulla
Mad Scientist,

Risposte:


10

C'è un modo per implementarlo dichiarativamente solo senza cambiare molto la configurazione attuale, se si accetta di introdurre un po 'di ridondanza. Ciò che segue può essere considerato uno sviluppo del suggerimento di RDFozz , anche se l'idea mi è completamente entrata in mente prima di leggere la sua risposta (ed è abbastanza diversa da giustificare comunque il proprio post di risposta).

Implementazione

Ecco cosa fai, passo dopo passo:

  1. Crea una containerTypestabella seguendo le linee di quella suggerita nella risposta di RDFozz:

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

    Popolarlo con ID predefiniti per ogni tipo. Ai fini di questa risposta, abbina l'esempio di RDFozz: 1 per le piante, 2 per gli animali, 3 per i batteri.

  2. Aggiungi una containerType_idcolonna containerse rendila non nullable e una chiave esterna.

    ALTER TABLE containers
    ADD containerType_id int NOT NULL
      REFERENCES containerTypes (id);
    
  3. Supponendo che la idcolonna sia già la chiave primaria di containers, creare un vincolo univoco su (id, containerType_id).

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

    Qui iniziano i licenziamenti. Se idviene dichiarata la chiave primaria, possiamo essere certi che è unica. Se è unico, qualsiasi combinazione di ide un'altra colonna è destinata a essere unica anche senza ulteriore dichiarazione di unicità - quindi, qual è il punto? Il punto è che dichiarando formalmente la coppia di colonne univoca, possiamo renderli referibili , ovvero essere il bersaglio di un vincolo di chiave esterna, di cui tratta questa parte.

  4. Aggiungere una containerType_idcolonna per ciascuna delle tabelle di giunzione ( containers_animals, containers_plants, containers_bacteria). Renderlo una chiave esterna è completamente opzionale. Ciò che è cruciale è assicurarsi che la colonna abbia lo stesso valore per tutte le righe, diversa per ogni tabella: 1 per containers_plants, 2 per containers_animals, 3 per containers_bacteria, secondo le descrizioni in containerTypes. In ogni caso puoi anche rendere quel valore predefinito per semplificare le tue istruzioni insert:

    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. In ciascuna delle tabelle di giunzione, rendere la coppia di colonne (container_id, containerType_id)un riferimento di vincolo di chiave esterna 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);
    

    Se container_idè già stato definito un riferimento a containers, sentiti libero di rimuovere quel vincolo da ogni tabella come non più necessario.

Come funziona

Aggiungendo la colonna del tipo di contenitore e facendola partecipare ai vincoli della chiave esterna, si prepara un meccanismo che impedisce la modifica del tipo di contenitore. La modifica del tipo nel containerstipo sarebbe possibile solo se le chiavi esterne fossero definite con la DEFERRABLEclausola, che non dovrebbero essere presenti in questa implementazione.

Anche se fossero differibili, la modifica del tipo sarebbe comunque impossibile a causa del vincolo di controllo sull'altro lato della containersrelazione della tabella di giunzione. Ogni tabella di giunzione consente solo un tipo di contenitore specifico. Ciò non solo impedisce ai riferimenti esistenti di modificare il tipo, ma impedisce anche l' aggiunta di riferimenti di tipo errati. Cioè, se hai un contenitore di tipo 2 (animali), puoi solo aggiungere elementi ad esso usando la tabella in cui è consentito il tipo 2, che è containers_animalse non sarebbe in grado di aggiungere righe a cui fa riferimento, diciamo containers_bacteria, che accetta solo contenitori di tipo 3.

Infine, la tua decisione di avere tabelle diverse per plants, animalse bacteria, e tabelle di giunzione diverse per ciascun tipo di entità, rende già impossibile per un contenitore avere elementi di più di un tipo.

Quindi, tutti questi fattori combinati assicurano, in modo puramente dichiarativo, che tutti i vostri contenitori saranno omogenei.


3

Un'opzione è aggiungere containertype_ida alla Containertabella. Rendi la colonna NOT NULL e una chiave esterna per una ContainerTypetabella, che avrebbe voci per ogni tipo di elemento che può andare in un contenitore:

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

Per assicurarsi che il tipo di contenitore non possa essere modificato, creare un trigger di aggiornamento che controlla se è containertype_idstato aggiornato e ripristina la modifica in quel caso.

Quindi, nei trigger di inserimento e aggiornamento nelle tabelle dei collegamenti contenitore, controlla containertype_id rispetto al tipo di entità in quella tabella, per assicurarti che corrispondano.

Se qualsiasi cosa tu abbia inserito in un contenitore deve corrispondere al tipo e il tipo non può essere modificato, allora tutto nel contenitore sarà dello stesso tipo.

NOTA: poiché il trigger nelle tabelle dei collegamenti è ciò che deciderà quale corrispondenza, se fosse necessario disporre di un tipo di contenitore che potesse contenere piante e animali, è possibile creare quel tipo, assegnarlo al contenitore e verificare che . Quindi, mantieni la flessibilità se le cose cambiano un po '(diciamo, ottieni i tipi "riviste" e "libri" ...).

NOTA il secondo: se la maggior parte di ciò che accade ai contenitori è lo stesso, indipendentemente da ciò che li contiene, allora ha senso. Se ci sono cose molto diverse che accadono (nel sistema, non nella nostra realtà fisica) in base al contenuto del contenitore, l'idea di Evan Carroll di avere tabelle separate per i tipi di contenitori separati ha perfettamente senso. Questa soluzione stabilisce che i contenitori hanno tipi diversi al momento della creazione, ma li mantiene nella stessa tabella. Se devi controllare il tipo ogni volta che esegui un'azione su un contenitore e se l'azione che fai dipende dal tipo, le tabelle separate potrebbero effettivamente essere più veloci e più facili.


È un modo per farlo, ma ci sono molti aspetti negativi: per fare ciò sono necessarie tre scansioni dell'indice per riassemblare l'elenco di contenitori / piante, rallenta gli inserimenti aggiungendo una selezione in una tabella esterna, riduce l'integrità in funzione di si innesca - a volte funziona ma non lo desidererei mai, inoltre rallenta gli aggiornamenti per assicurarsi che la colonna non sia modificata. Detto questo, penso che stiamo lavorando intorno al blocco mentale più che soddisfare le richieste di un'app, ma dai voti potrei essere solo in questo.
Evan Carroll,

1
Non sappiamo esattamente cosa deve succedere da qui; se la maggior parte dell'applicazione si concentra sui contenitori stessi (spedizione, tracciabilità, localizzazione in strutture di stoccaggio, ecc.), la maggior parte delle query potrebbe non essere focalizzata sul contenuto dei contenitori, ma solo sui contenitori stessi. Come ho notato, ci sono sicuramente scenari in cui ha senso trattare un contenitore di piante come un'entità completamente diversa da un contenitore di animali. L'OP dovrà decidere quale scenario devono affrontare.
RDFozz,

3

Se hai bisogno solo di 2 o 3 categorie (piante / metazoi / batteri) e vuoi modellare una relazione XOR, forse un "arco" è la soluzione per te. Vantaggio: nessuna necessità di trigger. Schemi di esempio sono disponibili [qui] [1]. Nella tua situazione, la tabella "contenitori" avrebbe 3 colonne con un vincolo CHECK, che consente una pianta o un animale o un batterio.

Questo probabilmente non è appropriato se in futuro sarà necessario distinguere tra molte categorie (ad es. Generi, specie, sottospecie). Tuttavia, per 2-3 gruppi / categorie questo può fare il trucco.

AGGIORNAMENTO: ispirato ai suggerimenti e ai commenti del contributore, una soluzione diversa che consente molti taxa (gruppi di organismi correlati, classificati dal biologo) ed evita nomi di tabelle "specifici" (PostgreSQL 9.5).

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

Dati di test:

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

test:

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

Grazie a @RDFozz e @Evan Carroll e @ypercube per il loro contributo e la loro pazienza (leggere / correggere le mie risposte).


1

In primo luogo, sono d'accordo con @RDFozz sulla lettura della domanda. Tuttavia, solleva alcune preoccupazioni sulla risposta degli stefani ,

inserisci qui la descrizione dell'immagine

Per rispondere alle sue preoccupazioni, giusto

  1. Rimuovi il PRIMARY KEY
  2. Aggiungi i UNIQUEvincoli per proteggere da voci duplicate.
  3. Aggiungi EXCLUSIONvincoli per garantire che i contenitori siano "omogenei"
  4. Aggiungi un indice c_idper garantire prestazioni decenti.
  5. Uccidi chiunque lo faccia, indicalo alla mia altra risposta per la sanità mentale.

Ecco come appare

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

Ora puoi avere un contenitore con più cose, ma solo un tipo di cosa in un contenitore.

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

Ed è tutto implementato sugli indici GIST.

La Grande Piramide di Giza non ha nulla su PostgreSQL.


0

Ho dei contenitori da tavolo che possono avere relazioni molti-a-molti con diversi tavoli, diciamo che sono piante, animali e batteri.

Questa è una cattiva idea.

Ma con questo schema, non riesco a trovare il modo di implementare il vincolo che i contenitori dovrebbero essere omogenei.

E ora sai perché. =)

Credo che tu sia bloccato sull'idea di eredità dalla programmazione orientata agli oggetti (OO). OO Inheritance risolve un problema con il riutilizzo del codice. In SQL, il codice ridondante è l' ultimo dei nostri problemi. L'integrità è innanzitutto. Le prestazioni sono spesso seconde. Ci godremo il dolore per i primi due. Non abbiamo un "tempo di compilazione" in grado di eliminare i costi.

Quindi rinuncia alla tua ossessione per il riutilizzo del codice. I contenitori per piante, animali e batteri sono fondamentalmente diversi in ogni parte del mondo reale. Il componente di riutilizzo del codice di "contiene cose" semplicemente non lo farà per te. Rompili. Non solo ti garantirà una maggiore integrità e maggiori prestazioni, ma in futuro troverai più semplice espandere il tuo schema: dopotutto, nel tuo schema dovevi già dividere gli elementi contenuti (piante, animali, ecc.) , sembra almeno possibile che dovrai rompere i contenitori. Allora non vorrai riprogettare l'intero schema.


La suddivisione dei contenitori sposterebbe il problema in una parte diversa dello schema, ho ancora bisogno di fare riferimento ai contenitori da altre tabelle e quelle parti dovrebbero distinguere anche i diversi tipi di contenitore.
Mad Scientist,

Saprebbero in quale tipo di contenitore hanno semplicemente il tavolo in cui trovano il contenitore. Sono confuso su cosa intendi? Le piante fanno riferimento a un singolo contenitore plant_containerse così via. Le cose che richiedono solo un contenitore per piante selezionano solo dalla plant_containerstabella. Le cose che necessitano di qualsiasi contenitore (es. Ricerca di tutti i tipi di contenitori) possono fare UNION ALLsu tutte e tre le tabelle con contenitori.
Evan Carroll,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.