Chiave esterna per più tabelle


127

Ho 3 tabelle pertinenti nel mio database.

CREATE TABLE dbo.Group
(
    ID int NOT NULL,
    Name varchar(50) NOT NULL
)  

CREATE TABLE dbo.User
(
    ID int NOT NULL,
    Name varchar(50) NOT NULL
)

CREATE TABLE dbo.Ticket
(
    ID int NOT NULL,
    Owner int NOT NULL,
    Subject varchar(50) NULL
)

Gli utenti appartengono a più gruppi. Ciò avviene tramite una relazione da molte a molte, ma irrilevante in questo caso. Un ticket può essere di proprietà di un gruppo o di un utente, tramite il campo dbo.Ticket.Owner.

Quale sarebbe il modo PIÙ CORRETTO per descrivere questa relazione tra un ticket e facoltativamente un utente o un gruppo?

Sto pensando che dovrei aggiungere una bandiera nella tabella dei ticket che dice che tipo possiede.


Secondo me ogni biglietto appartiene a un gruppo. È solo che un utente è un gruppo di uno. Quale scelta 4 dei modelli @ nathan-skerl. Se usi le Guide come chiavi, tutto funziona abbastanza bene
GraemeMiller l'

Risposte:


149

Hai alcune opzioni, tutte variabili in "correttezza" e facilità d'uso. Come sempre, il design giusto dipende dalle tue esigenze.

  • È possibile semplicemente creare due colonne in Ticket, OwnedByUserId e OwnedByGroupId e disporre di chiavi esterne nullable per ogni tabella.

  • È possibile creare tabelle di riferimento M: M abilitando sia ticket: utente sia ticket: relazioni di gruppo. Forse in futuro vorresti consentire a un singolo biglietto di essere posseduto da più utenti o gruppi? Questo progetto non impone che un biglietto debba essere di proprietà di una sola entità.

  • È possibile creare un gruppo predefinito per ogni utente e disporre di ticket semplicemente di proprietà di un vero gruppo o di un gruppo predefinito dell'utente.

  • Oppure (la mia scelta) modellare un'entità che funge da base sia per utenti che per gruppi e che possiede ticket di proprietà di tale entità.

Ecco un esempio approssimativo usando lo schema pubblicato:

create table dbo.PartyType
(   
    PartyTypeId tinyint primary key,
    PartyTypeName varchar(10)
)

insert into dbo.PartyType
    values(1, 'User'), (2, 'Group');


create table dbo.Party
(
    PartyId int identity(1,1) primary key,
    PartyTypeId tinyint references dbo.PartyType(PartyTypeId),
    unique (PartyId, PartyTypeId)
)

CREATE TABLE dbo.[Group]
(
    ID int primary key,
    Name varchar(50) NOT NULL,
    PartyTypeId as cast(2 as tinyint) persisted,
    foreign key (ID, PartyTypeId) references Party(PartyId, PartyTypeID)
)  

CREATE TABLE dbo.[User]
(
    ID int primary key,
    Name varchar(50) NOT NULL,
    PartyTypeId as cast(1 as tinyint) persisted,
    foreign key (ID, PartyTypeId) references Party(PartyID, PartyTypeID)
)

CREATE TABLE dbo.Ticket
(
    ID int primary key,
    [Owner] int NOT NULL references dbo.Party(PartyId),
    [Subject] varchar(50) NULL
)

7
Come sarebbe una query per i biglietti utente / gruppo? Grazie.
paulkon,

4
Qual è il vantaggio delle colonne calcolate persistenti nelle tabelle di gruppo e utente? La chiave primaria nella tabella di Party garantisce già che non vi siano sovrapposizioni tra ID gruppo e ID utente, quindi la chiave esterna deve essere solo sul PartyId da solo. Qualsiasi query scritta dovrebbe comunque conoscere le tabelle da PartyTypeName comunque.
Arin Taylor,

1
@ArinTaylor la colonna persistente ci impedisce di creare un utente di tipo Utente e di metterlo in relazione con un record in dbo.Group.
Nathan Skerl,

3
@paulkon So che questa è una vecchia domanda, ma la query sarebbe qualcosa di simile SELECT t.Subject AS ticketSubject, CASE WHEN u.Name IS NOT NULL THEN u.Name ELSE g.Name END AS ticketOwnerName FROM Ticket t INNER JOIN Party p ON t.Owner=p.PartyId LEFT OUTER JOIN User u ON u.ID=p.PartyId LEFT OUTER JOIN Group g on g.ID=p.PartyID;Nel risultato avresti ogni oggetto biglietto e nome del proprietario.
Corey McMahon,

2
Per quanto riguarda l'opzione 4, qualcuno può confermare se si tratta di un modello anti o di una soluzione per un modello anti?
Inckka,

31

La prima opzione nella lista di @Nathan Skerl è quella che è stata implementata in un progetto con cui ho lavorato una volta, in cui è stata stabilita una relazione simile tra tre tabelle. (Uno di questi faceva riferimento ad altri due, uno alla volta.)

Quindi, la tabella di riferimento aveva due colonne di chiave esterna e aveva anche un vincolo per garantire che a una sola riga (a cui non fosse stato assegnato alcun riferimento) fosse indicata una sola tabella.

Ecco come potrebbe apparire quando applicato alle tue tabelle:

CREATE TABLE dbo.[Group]
(
    ID int NOT NULL CONSTRAINT PK_Group PRIMARY KEY,
    Name varchar(50) NOT NULL
);

CREATE TABLE dbo.[User]
(
    ID int NOT NULL CONSTRAINT PK_User PRIMARY KEY,
    Name varchar(50) NOT NULL
);

CREATE TABLE dbo.Ticket
(
    ID int NOT NULL CONSTRAINT PK_Ticket PRIMARY KEY,
    OwnerGroup int NULL
      CONSTRAINT FK_Ticket_Group FOREIGN KEY REFERENCES dbo.[Group] (ID),
    OwnerUser int NULL
      CONSTRAINT FK_Ticket_User  FOREIGN KEY REFERENCES dbo.[User]  (ID),
    Subject varchar(50) NULL,
    CONSTRAINT CK_Ticket_GroupUser CHECK (
      CASE WHEN OwnerGroup IS NULL THEN 0 ELSE 1 END +
      CASE WHEN OwnerUser  IS NULL THEN 0 ELSE 1 END = 1
    )
);

Come puoi vedere, la Tickettabella ha due colonne OwnerGroupe OwnerUser, entrambe sono chiavi esterne nullable. (Le rispettive colonne nelle altre due tabelle sono rese di conseguenza chiavi primarie.) Il CK_Ticket_GroupUservincolo check assicura che solo una delle due colonne chiave esterna contenga un riferimento (l'altro è NULL, ecco perché entrambi devono essere nullable).

(La chiave primaria su Ticket.IDnon è necessaria per questa particolare implementazione, ma sicuramente non sarebbe dannoso averne una in una tabella come questa.)


1
Questo è anche ciò che abbiamo nel nostro software ed eviterei se stai cercando di creare un framework di accesso ai dati generico. Questo design aumenterà la complessità a livello di app.
Frank

4
Sono davvero nuovo a SQL, quindi correggimi se questo è sbagliato, ma questo design sembra essere un approccio da usare quando sei estremamente sicuro che avrai bisogno solo di due tipi di proprietario di un biglietto. In fondo alla strada se viene introdotto un terzo tipo di proprietario del ticket, è necessario aggiungere una terza colonna di chiave esterna nullable alla tabella.
Shadoninja,

@Shadoninja: non ti sbagli. In effetti, penso che sia un modo completamente giusto di dirlo. In genere sto bene con questo tipo di soluzione in cui è giustificato, ma certamente non sarebbe la prima cosa che penso quando si considerano le opzioni, proprio per il motivo che hai delineato.
Andriy M,

2
@ Frank.Germain In questo caso è possibile utilizzare una chiave esterna univoca basata su due colonne RefID, in RefTypecui RefTypeè presente un identificatore fisso della tabella di destinazione. Se hai bisogno di integrità, puoi eseguire controlli nel trigger o nel livello app. In questo caso è possibile il recupero generico. SQL dovrebbe consentire una definizione FK come questa, facilitando la nostra vita.
djmj,

2

Un'altra opzione è quella di avere, in Ticket, una colonna che specifica il tipo di entità proprietario ( Usero Group), la seconda colonna con riferimento Usero Groupid e NON utilizzare chiavi esterne ma invece fare affidamento su un trigger per applicare l'integrità referenziale.

Due vantaggi che vedo qui sull'eccellente modello di Nathan (sopra):

  • Chiarezza e semplicità più immediate.
  • Query più semplici da scrivere.

1
Ma questo non consentirebbe una chiave esterna, giusto? Sto ancora cercando di capire il design giusto per il mio progetto attuale, in cui una tabella può fare riferimento ad almeno 3 forse più in futuro
Can Rau,

2

Un altro approccio è quello di creare una tabella di associazione che contenga colonne per ogni potenziale tipo di risorsa. Nel tuo esempio, ciascuno dei due tipi di proprietario esistenti ha una propria tabella (il che significa che hai qualcosa a cui fare riferimento). Se questo sarà sempre il caso, puoi avere qualcosa del genere:

CREATE TABLE dbo.Group
(
    ID int NOT NULL,
    Name varchar(50) NOT NULL
)  

CREATE TABLE dbo.User
(
    ID int NOT NULL,
    Name varchar(50) NOT NULL
)

CREATE TABLE dbo.Ticket
(
    ID int NOT NULL,
    Owner_ID int NOT NULL,
    Subject varchar(50) NULL
)

CREATE TABLE dbo.Owner
(
    ID int NOT NULL,
    User_ID int NULL,
    Group_ID int NULL,
    {{AdditionalEntity_ID}} int NOT NULL
)

Con questa soluzione, continueresti ad aggiungere nuove colonne mentre aggiungi nuove entità al database e elimineresti e ricreare il modello di vincolo di chiave esterna mostrato da @Nathan Skerl. Questa soluzione è molto simile a @Nathan Skerl ma ha un aspetto diverso (fino alle preferenze).

Se non hai intenzione di avere una nuova tabella per ogni nuovo tipo di proprietario, forse sarebbe bene includere un proprietario_tipo invece di una colonna di chiave esterna per ogni potenziale proprietario:

CREATE TABLE dbo.Group
(
    ID int NOT NULL,
    Name varchar(50) NOT NULL
)  

CREATE TABLE dbo.User
(
    ID int NOT NULL,
    Name varchar(50) NOT NULL
)

CREATE TABLE dbo.Ticket
(
    ID int NOT NULL,
    Owner_ID int NOT NULL,
    Owner_Type string NOT NULL, -- In our example, this would be "User" or "Group"
    Subject varchar(50) NULL
)

Con il metodo sopra, è possibile aggiungere tutti i tipi di proprietario desiderati. Owner_ID non avrebbe un vincolo di chiave esterna ma verrebbe utilizzato come riferimento alle altre tabelle. Il rovescio della medaglia è che dovresti guardare la tabella per vedere quali tipi di proprietario ci sono poiché non è immediatamente ovvio in base allo schema. Lo suggerirei solo se non conosci in anticipo i tipi di proprietario e non si collegheranno ad altre tabelle. Se conosci in anticipo i tipi di proprietario, sceglierei una soluzione come @Nathan Skerl.

Scusate se ho sbagliato SQL, ho appena lanciato questo.


-4
CREATE TABLE dbo.OwnerType
(
    ID int NOT NULL,
    Name varchar(50) NULL
)

insert into OwnerType (Name) values ('User');
insert into OwnerType (Name) values ('Group');

Penso che sarebbe il modo più generale per rappresentare ciò che vuoi invece di usare una bandiera.

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.