Come avere una relazione uno-a-molti con un figlio privilegiato?


22

Voglio avere una relazione uno-a-molti in cui per ogni genitore, uno o zero dei figli è contrassegnato come "favorito". Tuttavia, non tutti i genitori avranno un figlio. (Pensa ai genitori come domande su questo sito, ai bambini come risposte e ai preferiti come risposta accettata.) Ad esempio,

TableA
    Id            INT PRIMARY KEY

TableB
    Id            INT PRIMARY KEY
    Parent        INT NOT NULL FOREIGN KEY REFERENCES TableA.Id

A mio modo di vedere, posso aggiungere la seguente colonna a TableA:

    FavoriteChild INT NULL FOREIGN KEY REFERENCES TableB.Id

o la seguente colonna di TableB:

    IsFavorite    BIT NOT NULL

Il problema con il primo approccio è che introduce una chiave esterna nullable, che, a quanto ho capito, non è in forma normalizzata. Il problema con il secondo approccio è che occorre lavorare di più per garantire che al massimo un bambino sia il preferito.

Che tipo di criteri dovrei usare per determinare quale approccio usare? Oppure ci sono altri approcci che non sto prendendo in considerazione?

Sto usando SQL Server 2012.

Risposte:


19

Un altro modo (senza Null e senza cicli nelle FOREIGN KEYrelazioni) è di avere una terza tabella per memorizzare i "figli preferiti". Nella maggior parte dei DBMS, avrai bisogno di un ulteriore UNIQUEvincolo TableB.

@Aaron è stato più veloce nell'identificare che la convenzione di denominazione sopra è piuttosto ingombrante e può portare a errori. Di solito è meglio (e ti manterrà sano) se non hai Idcolonne su tutte le tue tabelle e se le colonne (che sono unite) hanno gli stessi nomi nelle molte tabelle che appaiono. Quindi, ecco una ridenominazione:

Parent
    ParentID        INT NOT NULL PRIMARY KEY

Child
    ChildID         INT NOT NULL PRIMARY KEY
    ParentID        INT NOT NULL FOREIGN KEY REFERENCES Parent (ParentID)
    UNIQUE (ParentID, ChildID)

FavoriteChild
    ParentID        INT NOT NULL PRIMARY KEY
    ChildID         INT NOT NULL 
    FOREIGN KEY (ParentID, ChildID) 
        REFERENCES Child (ParentID, ChildID)

In SQL Server (che stai utilizzando) hai anche l'opzione della IsFavoritecolonna di bit che menzioni. Il figlio unico preferito per genitore può essere realizzato tramite un indice univoco filtrato:

Parent
    ParentID        INT NOT NULL PRIMARY KEY

Child
    ChildID         INT NOT NULL PRIMARY KEY
    ParentID        INT NOT NULL FOREIGN KEY REFERENCES Parent (ParentID)
    IsFavorite      BIT NOT NULL

CREATE UNIQUE INDEX is_FavoriteChild
  ON Child (ParentID)
  WHERE IsFavorite = 1 ;

E il motivo principale per cui l'opzione 1 non è consigliata, almeno non in SQL Server, è che il modello di percorsi circolari nei riferimenti a chiave esterna presenta alcuni problemi.

Leggi un articolo piuttosto vecchio: SQL By Design: The Circular Reference

Quando si inseriscono o si eliminano le righe dalle due tabelle, si verificherà il problema "pollo e uovo". Quale tabella devo inserire per prima, senza violare alcun vincolo?

Per risolverlo, devi definire almeno una colonna nullable. (OK, tecnicamente non è necessario, puoi avere tutte le colonne come, NOT NULLma solo in DBMS, come Postgres e Oracle, che hanno implementato vincoli differibili. Vedi la risposta di @ Erwin in una domanda simile: Vincolo di chiave esterna complessa in SQLAlchemy su come questo può essere fatto in Postgres). Tuttavia, questa configurazione sembra pattinare sul ghiaccio sottile.

Controlla anche una domanda quasi identica su SO (ma per MySQL) In SQL, è giusto che due tabelle si facciano riferimento? dove la mia risposta è praticamente la stessa. MySQL non ha indici parziali, quindi le uniche opzioni praticabili sono l'FK nullable e la soluzione per tabelle extra.


9

Dipende da quale è la tua priorità. Vuoi evitare il lavoro o vuoi aderire alle più rigide regole di normalizzazione?

Personalmente, penso che sia meglio averlo IsFavoritenella tabella dei figli e sarei disposto a lavorare per assicurarsi che al massimo un figlio per ogni genitore sia il favorito di quel genitore. Il motivo principale non ha nulla a che fare con la cosa nullable chiave esterna: non mi piace l'idea di tutte le chiavi esterne che puntano in entrambe le direzioni.

Il suggerimento di @ ypercube è anche un buon compromesso.

A parte, per favore, per favore, per favore, non sporcare il tuo schema con nomi di colonne insignificanti come Id. Preferirei di gran lunga vedere il Idnome in modo significativo in tutto lo schema. Identifica un autore? Ok, chiamalo AuthorID. Rappresenta un prodotto? Ok, ProductID. È un dipendente e in alcuni casi fa riferimento a un manager? Ok, EmployeeIDe ha ManagerIDpiù senso per me di IDe Parent. Mentre può sembrare logico lasciarlo fuori (e ridondante per inserirlo), quando inizi a scrivere join complessi (o pubblica query qui) sentirai sicuramente delle imprecazioni quando stai cercando di decodificare un gruppo di join in quel punto a colonne come a.Parent = b.ID... blecch.


1

I dati appartengono alla tabella figlio. Lo manteniamo corretto con un trigger sul tavolo per garantire che un solo record sia contrassegnato come preferito (o nel nostro caso come indirizzo preferito).

Tuttavia, anche l'idea di @ ypercube di una tabella separata è buona.

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.