Clé étrangère vers plusieurs tables

127

J'ai 3 tables pertinentes dans ma base de données.

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
)

Les utilisateurs appartiennent à plusieurs groupes. Cela se fait via une relation plusieurs à plusieurs, mais sans importance dans ce cas. Un ticket peut appartenir à un groupe ou à un utilisateur, via le champ dbo.Ticket.Owner.

Quel serait le PLUS CORRECT façon de décrire cette relation entre un ticket et éventuellement un utilisateur ou un groupe?

Je pense que je devrais ajouter un drapeau dans la table des tickets qui indique quel type le possède.

Darthg8r
la source
À mon avis, chaque billet appartient à un groupe. C'est juste qu'un utilisateur est un groupe d'un. Quel choix 4 parmi les modèles @ nathan-skerl. Si vous utilisez des guides comme clés, tout fonctionne également très bien
GraemeMiller

Réponses:

149

Vous avez quelques options, toutes variant en "exactitude" et en facilité d'utilisation. Comme toujours, la bonne conception dépend de vos besoins.

  • Vous pouvez simplement créer deux colonnes dans Ticket, OwnedByUserId et OwnedByGroupId, et avoir des clés étrangères nullables pour chaque table.

  • Vous pouvez créer des tables de référence M: M permettant à la fois les relations ticket: utilisateur et ticket: groupe. Peut-être voudrez-vous à l'avenir autoriser un seul ticket à appartenir à plusieurs utilisateurs ou groupes? Cette conception n'impose pas qu'un ticket doit appartenir à une seule entité uniquement.

  • Vous pouvez créer un groupe par défaut pour chaque utilisateur et avoir des tickets appartenant simplement à un vrai groupe ou au groupe par défaut d'un utilisateur.

  • Ou (mon choix) modélisez une entité qui sert de base à la fois aux utilisateurs et aux groupes, et dont les billets sont la propriété de cette entité.

Voici un exemple approximatif utilisant votre schéma publié:

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
la source
7
À quoi ressemblerait une requête pour les tickets utilisateur / groupe? Merci.
paulkon
4
Quel est l'avantage des colonnes calculées persistantes dans les tables de groupe et d'utilisateur? La clé primaire dans la table Party garantit déjà qu'il n'y aura pas de chevauchement dans les ID de groupe et les ID d'utilisateur, de sorte que la clé étrangère doit uniquement être sur le PartyId seul. Toutes les requêtes écrites auraient quand même besoin de connaître les tables de PartyTypeName.
Arin Taylor
1
@ArinTaylor la colonne persistante nous empêche de créer un Party de type User et de le relier à un enregistrement dans dbo.Group.
Nathan Skerl
3
@paulkon Je sais que c'est une vieille question, mais la requête serait quelque chose comme 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;Dans le résultat, vous auriez chaque sujet de ticket et le nom du propriétaire.
Corey McMahon
2
En ce qui concerne l'option 4, quelqu'un peut-il confirmer s'il s'agit d'un anti-motif ou d'une solution pour un anti-motif?
inckka
31

La première option dans la liste de @Nathan Skerl est ce qui a été implémenté dans un projet avec lequel j'ai travaillé une fois, où une relation similaire a été établie entre trois tables. (L'un d'eux en a fait référence à deux autres, un à la fois.)

Ainsi, la table de référence avait deux colonnes de clé étrangère, et elle avait également une contrainte pour garantir qu'exactement une table (ni les deux, ni aucune) était référencée par une seule ligne.

Voici à quoi cela pourrait ressembler lorsqu'il est appliqué à vos tableaux:

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

Comme vous pouvez le voir, la Tickettable comporte deux colonnes OwnerGroupet OwnerUser, qui sont toutes deux des clés étrangères Nullable. (Les colonnes respectives des deux autres tables deviennent des clés primaires en conséquence.) La CK_Ticket_GroupUsercontrainte de vérification garantit que seule l'une des deux colonnes de clé étrangère contient une référence (l'autre étant NULL, c'est pourquoi les deux doivent être NULL).

(La clé primaire sur Ticket.IDn'est pas nécessaire pour cette implémentation particulière, mais cela ne ferait certainement pas de mal d'en avoir une dans une table comme celle-ci.)

Andriy M
la source
1
C'est également ce que nous avons dans notre logiciel et j'éviterais si vous essayez de créer un cadre générique d'accès aux données. Cette conception augmentera la complexité de la couche d'application.
Frank.Germain
4
Je suis vraiment nouveau dans SQL, alors corrigez-moi si ce n'est pas le cas, mais cette conception semble être une approche à utiliser lorsque vous êtes extrêmement confiant que vous n'aurez besoin que de deux types de propriétaire d'un ticket. Plus tard, si un troisième type de propriétaire de ticket était introduit, vous devrez ajouter une troisième colonne de clé étrangère Nullable à la table.
Shadoninja
@Shadoninja: Vous n'avez pas tort. En fait, je pense que c'est une façon tout à fait juste de le dire. Je suis généralement d'accord avec ce genre de solution là où c'est justifié, mais ce ne serait certainement pas la première dans mon esprit lors de l'examen des options - précisément à cause de la raison que vous avez indiquée.
Andriy M
2
@ Frank.Germain Dans ce cas, vous pouvez utiliser une clé étrangère unique basée sur deux colonnes RefID, RefTypeRefTypeest un identifiant fixe de la table cible. Si vous avez besoin d'intégrité, vous pouvez effectuer des vérifications dans le déclencheur ou la couche d'application. La récupération générique est possible dans ce cas. SQL devrait permettre une définition FK comme celle-ci, ce qui nous facilite la vie.
djmj
2

Une autre option encore consiste à avoir, dans Ticket, une colonne spécifiant le type d'entité propriétaire ( Userou Group), une deuxième colonne avec référencé Userou Groupid et de NE PAS utiliser de clés étrangères, mais plutôt de s'appuyer sur un déclencheur pour appliquer l'intégrité référentielle.

Deux avantages que je vois ici par rapport à l' excellent modèle de Nathan (ci-dessus):

  • Plus de clarté et de simplicité immédiates.
  • Requêtes plus simples à écrire.
Jan Żankowski
la source
1
Mais cela ne permettrait pas une clé étrangère, non? J'essaie toujours de trouver le bon design pour mon projet actuel, où une table peut référencer au moins 3 peut-être plus à l'avenir
Can Rau
2

Une autre approche consiste à créer une table d'association contenant des colonnes pour chaque type de ressource potentiel. Dans votre exemple, chacun des deux types de propriétaires existants a sa propre table (ce qui signifie que vous avez quelque chose à référencer). Si tel est toujours le cas, vous pouvez avoir quelque chose comme ceci:

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
)

Avec cette solution, vous continueriez à ajouter de nouvelles colonnes à mesure que vous ajoutez de nouvelles entités à la base de données et vous supprimeriez et recréeriez le modèle de contrainte de clé étrangère affiché par @Nathan Skerl. Cette solution est très similaire à @Nathan Skerl mais semble différente (selon vos préférences).

Si vous n'allez pas avoir une nouvelle table pour chaque nouveau type de propriétaire, il serait peut-être bon d'inclure un owner_type au lieu d'une colonne de clé étrangère pour chaque propriétaire potentiel:

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
)

Avec la méthode ci-dessus, vous pouvez ajouter autant de types de propriétaires que vous le souhaitez. Owner_ID n'aurait pas de contrainte de clé étrangère mais serait utilisé comme référence aux autres tables. L'inconvénient est que vous devriez regarder le tableau pour voir quels types de propriétaires il y a car ce n'est pas immédiatement évident en fonction du schéma. Je ne suggérerais cela que si vous ne connaissez pas les types de propriétaires à l'avance et qu'ils ne seront pas liés à d'autres tables. Si vous connaissez à l'avance les types de propriétaires, j'irais avec une solution comme @Nathan Skerl.

Désolé si je me trompe de SQL, je viens de jeter ceci ensemble.

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

Je pense que ce serait la manière la plus générale de représenter ce que vous voulez au lieu d'utiliser un drapeau.

Francisco Soto
la source