Fremdschlüssel für mehrere Tabellen

127

Ich habe 3 relevante Tabellen in meiner Datenbank.

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
)

Benutzer gehören mehreren Gruppen an. Dies geschieht über eine Beziehung von vielen zu vielen, ist in diesem Fall jedoch irrelevant. Ein Ticket kann entweder einer Gruppe oder einem Benutzer über das Feld dbo.Ticket.Owner gehören.

Wie würde diese Beziehung zwischen einem Ticket und optional einem Benutzer oder einer Gruppe am RICHTIGSTEN beschrieben?

Ich denke, dass ich ein Flag in die Ticket-Tabelle einfügen sollte, das besagt, welcher Typ es besitzt.

Darthg8r
quelle
Meiner Meinung nach gehört jedes Ticket einer Gruppe. Es ist nur so, dass ein Benutzer eine Gruppe von einem ist. Welche Wahl 4 von @ Nathan-Skerl-Modellen. Wenn Sie Guids als Schlüssel verwenden, funktioniert das Ganze auch ganz gut
GraemeMiller

Antworten:

149

Sie haben einige Optionen, die sich alle in "Korrektheit" und Benutzerfreundlichkeit unterscheiden. Wie immer hängt das richtige Design von Ihren Bedürfnissen ab.

  • Sie können einfach zwei Spalten in Ticket erstellen, OwnedByUserId und OwnedByGroupId, und jeder Tabelle nullbare Fremdschlüssel zuweisen.

  • Sie können M: M-Referenztabellen erstellen, die sowohl Ticket: Benutzer- als auch Ticket: Gruppenbeziehungen aktivieren. Vielleicht möchten Sie in Zukunft zulassen, dass ein einzelnes Ticket mehreren Benutzern oder Gruppen gehört? Dieses Design erzwingt nicht, dass ein Ticket nur einer einzelnen Entität gehören darf .

  • Sie können für jeden Benutzer eine Standardgruppe erstellen und Tickets haben, die entweder einer echten Gruppe oder der Standardgruppe eines Benutzers gehören.

  • Oder (meine Wahl) eine Entität modellieren, die als Basis für Benutzer und Gruppen fungiert und über Tickets verfügt, die dieser Entität gehören.

Hier ist ein grobes Beispiel mit Ihrem veröffentlichten Schema:

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
)
Nathan Skerl
quelle
7
Wie würde eine Abfrage nach Benutzer- / Gruppentickets aussehen? Vielen Dank.
Paulkon
4
Was ist der Vorteil der persistierten berechneten Spalten in Gruppen- und Benutzertabellen? Der Primärschlüssel in der Party-Tabelle stellt bereits sicher, dass sich die Gruppen- und Benutzer-IDs nicht überschneiden. Der Fremdschlüssel muss sich also nur auf der PartyId befinden. Alle geschriebenen Abfragen müssten die Tabellen trotzdem aus dem PartyTypeName kennen.
Arin Taylor
1
@ArinTaylor Die persistierte Spalte verhindert, dass wir eine Partei vom Typ Benutzer erstellen und sie mit einem Datensatz in dbo.Group verknüpfen.
Nathan Skerl
3
@paulkon Ich weiß, dass dies eine alte Frage ist, aber die Abfrage wäre so etwas wie 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;Im Ergebnis hätten Sie jeden Betreff und Namen des Tickets.
Corey McMahon
2
Kann jemand in Bezug auf Option 4 bestätigen, ob dies ein Anti-Muster oder eine Lösung für ein Anti-Muster ist?
Inckka
31

Die erste Option in der Liste von @Nathan Skerl ist die Implementierung in einem Projekt, mit dem ich einmal gearbeitet habe und in dem eine ähnliche Beziehung zwischen drei Tabellen hergestellt wurde. (Einer von ihnen verwies nacheinander auf zwei andere.)

Die Referenzierungstabelle hatte also zwei Fremdschlüsselspalten und eine Einschränkung, um sicherzustellen, dass genau eine Tabelle (nicht beide, nicht keine) von einer einzelnen Zeile referenziert wurde.

So könnte es aussehen, wenn es auf Ihre Tabellen angewendet wird:

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

Wie Sie sehen können, enthält die TicketTabelle zwei Spalten OwnerGroupund OwnerUserbeide sind nullbare Fremdschlüssel. (Die entsprechenden Spalten in den beiden anderen Tabellen werden entsprechend zu Primärschlüsseln.) Die CK_Ticket_GroupUserPrüfbedingung stellt sicher, dass nur eine der beiden Fremdschlüsselspalten eine Referenz enthält (die andere ist NULL, daher müssen beide nullwertfähig sein).

(Der Primärschlüssel Ticket.IDist für diese bestimmte Implementierung nicht erforderlich, aber es würde definitiv nicht schaden, einen in einer solchen Tabelle zu haben.)

Andriy M.
quelle
1
Dies ist auch das, was wir in unserer Software haben, und ich würde es vermeiden, wenn Sie versuchen, ein generisches Datenzugriffs-Framework zu erstellen. Dieses Design erhöht die Komplexität in der App-Ebene.
Frank.Germain
4
Ich bin wirklich neu in SQL, also korrigieren Sie mich, wenn dies falsch ist, aber dieses Design scheint ein Ansatz zu sein, wenn Sie äußerst sicher sind, dass Sie nur zwei Besitzertypen eines Tickets benötigen. Wenn später ein dritter Ticketbesitzertyp eingeführt würde, müssten Sie der Tabelle eine dritte nullbare Fremdschlüsselspalte hinzufügen.
Shadoninja
@ Shadoninja: Du liegst nicht falsch. Tatsächlich denke ich, dass dies eine völlig faire Art ist, es auszudrücken. Ich bin im Allgemeinen mit dieser Art von Lösung einverstanden, wenn sie gerechtfertigt ist, aber ich würde sie sicherlich nicht als erstes in Betracht ziehen, wenn ich über Optionen nachdenke - genau aus dem Grund, den Sie dargelegt haben.
Andriy M
2
@ Frank.Germain In diesem Fall können Sie einen eindeutigen Fremdschlüssel verwenden, der auf zwei Spalten basiert RefID, RefTypewobei RefTypees sich um eine feste Kennung der Zieltabelle handelt . Wenn Sie Integrität benötigen, können Sie Überprüfungen in der Trigger- oder App-Ebene durchführen. In diesem Fall ist ein generisches Abrufen möglich. SQL sollte eine solche FK-Definition ermöglichen und unser Leben erleichtern.
DJJJ
2

Eine weitere Option besteht darin, in Ticketeiner Spalte den Typ ( Useroder Group) der besitzenden Entität anzugeben , eine zweite Spalte mit Referenz Useroder GroupID anzugeben und KEINE Fremdschlüssel zu verwenden, sondern sich stattdessen auf einen Trigger zu verlassen, um die referenzielle Integrität zu erzwingen.

Zwei Vorteile, die ich hier gegenüber Nathans ausgezeichnetem Modell (oben) sehe :

  • Mehr unmittelbare Klarheit und Einfachheit.
  • Einfachere Abfragen zum Schreiben.
Jan Żankowski
quelle
1
Aber das würde keinen Fremdschlüssel erlauben, oder? Ich versuche immer noch, das richtige Design für mein aktuelles Projekt zu finden, bei dem eine Tabelle in Zukunft auf mindestens 3 oder mehr verweisen kann
Can Rau
2

Ein anderer Ansatz besteht darin, eine Zuordnungstabelle zu erstellen, die Spalten für jeden potenziellen Ressourcentyp enthält. In Ihrem Beispiel hat jeder der beiden vorhandenen Eigentümertypen eine eigene Tabelle (was bedeutet, dass Sie auf etwas verweisen müssen). Wenn dies immer der Fall sein wird, können Sie so etwas haben:

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
)

Mit dieser Lösung würden Sie weiterhin neue Spalten hinzufügen, wenn Sie der Datenbank neue Entitäten hinzufügen, und Sie würden das von @Nathan Skerl angezeigte Fremdschlüsseleinschränkungsmuster löschen und neu erstellen. Diese Lösung ist @Nathan Skerl sehr ähnlich, sieht aber anders aus (je nach Präferenz).

Wenn Sie nicht für jeden neuen Besitzertyp eine neue Tabelle haben, ist es möglicherweise sinnvoll, für jeden potenziellen Eigentümer einen Eigentümertyp anstelle einer Fremdschlüsselspalte anzugeben:

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
)

Mit der obigen Methode können Sie so viele Besitzertypen hinzufügen, wie Sie möchten. Owner_ID hätte keine Fremdschlüsseleinschränkung, sondern würde als Referenz auf die anderen Tabellen verwendet. Der Nachteil ist, dass Sie in der Tabelle nachsehen müssen, welche Eigentümertypen es gibt, da dies anhand des Schemas nicht sofort ersichtlich ist. Ich würde dies nur vorschlagen, wenn Sie die Besitzertypen vorher nicht kennen und sie nicht mit anderen Tabellen verknüpft werden. Wenn Sie die Eigentümertypen im Voraus kennen, würde ich eine Lösung wie @Nathan Skerl wählen.

Tut mir leid, wenn ich etwas falsches SQL habe, habe ich das einfach zusammengeschmissen.

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

Ich denke, das wäre die allgemeinste Art, das darzustellen, was Sie wollen, anstatt eine Flagge zu verwenden.

Francisco Soto
quelle