Chave estrangeira para várias tabelas

127

Eu tenho 3 tabelas relevantes no meu banco de dados.

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
)

Os usuários pertencem a vários grupos. Isso é feito através de um relacionamento muitos para muitos, mas irrelevante neste caso. Um ticket pode pertencer a um grupo ou usuário, através do campo dbo.Ticket.Owner.

Qual seria a maneira MAIS CORRETA de descrever esse relacionamento entre um ticket e, opcionalmente, um usuário ou um grupo?

Eu estou pensando que eu deveria adicionar um sinalizador na tabela de tickets que diz que tipo possui.

Darthg8r
fonte
Na minha opinião, cada ingresso pertence a um grupo. É apenas que um usuário é um grupo de um. Qual escolha 4 dos modelos @ nathan-skerl. Se você usar GUIDs como chaves de toda a coisa também funciona muito bem
GraemeMiller

Respostas:

149

Você tem algumas opções, todas variando em "correção" e facilidade de uso. Como sempre, o design certo depende das suas necessidades.

  • Você pode simplesmente criar duas colunas no Ticket, OwnedByUserId e OwnedByGroupId, e ter chaves estrangeiras anuláveis ​​em cada tabela.

  • Você pode criar tabelas de referência M: M, permitindo os relacionamentos ticket: usuário e ticket: grupo. Talvez no futuro você deseje permitir que um único ticket seja de propriedade de vários usuários ou grupos? Esse design não impõe que um ticket deva pertencer apenas a uma única entidade.

  • Você pode criar um grupo padrão para cada usuário e ter tickets simplesmente pertencentes a um grupo verdadeiro ou a um grupo padrão do usuário.

  • Ou (minha escolha) modele uma entidade que atue como base para usuários e grupos e tenha tickets pertencentes a essa entidade.

Aqui está um exemplo aproximado usando seu esquema publicado:

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
fonte
7
Como seria uma consulta para tickets de usuário / grupo? Obrigado.
paulkon
4
Qual é o benefício das colunas computadas persistentes nas tabelas Grupo e Usuário? A chave primária na tabela Parte já garante que não haverá sobreposição nas IDs de grupo e de usuário, portanto, a chave estrangeira precisa estar apenas no PartyId. Qualquer consulta escrita ainda precisaria conhecer as tabelas do PartyTypeName de qualquer maneira.
Arin Taylor
1
@ArinTaylor a coluna persistente nos impede de criar uma Parte do tipo Usuário e relacioná-la com um registro no dbo.Group.
precisa saber é o seguinte
3
@ paulkon Eu sei que essa é uma pergunta antiga, mas a consulta seria algo como 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;No resultado, você teria todos os assuntos e nome do proprietário do ticket.
Corey McMahon
2
Com relação à opção 4, alguém pode confirmar se esse é um antipadrão ou uma solução para um antipadrão?
inckka
31

A primeira opção na lista de @Nathan Skerl é o que foi implementado em um projeto com o qual trabalhei, onde um relacionamento semelhante foi estabelecido entre três tabelas. (Um deles referenciou outros dois, um de cada vez.)

Portanto, a tabela de referência tinha duas colunas de chave estrangeira e também havia uma restrição para garantir que exatamente uma tabela (não ambas, nem nenhuma) fosse referenciada por uma única linha.

Veja como ele pode parecer quando aplicado às suas tabelas:

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

Como você pode ver, a Tickettabela possui duas colunas OwnerGroupe OwnerUser, ambas são chaves estrangeiras anuláveis. (As respectivas colunas nas outras duas tabelas são transformadas em chaves primárias de acordo.) A CK_Ticket_GroupUserrestrição de verificação garante que apenas uma das duas colunas de chave estrangeira contenha uma referência (a outra sendo NULL, é por isso que ambas precisam ser anuláveis).

(A chave primária ativada Ticket.IDnão é necessária para esta implementação específica, mas definitivamente não faria mal ter uma em uma tabela como essa.)

Andriy M
fonte
1
Isso também é o que temos em nosso software e eu evitaria se você estivesse tentando criar uma estrutura genérica de acesso a dados. Esse design aumentará a complexidade na camada do aplicativo.
Frank.Germain
4
Eu sou realmente novo no SQL, então me corrija se isso estiver errado, mas esse design parece ser uma abordagem a ser usada quando você estiver extremamente confiante de que precisará apenas de dois tipos de proprietários de um ticket. No futuro, se um terceiro tipo de proprietário de ticket for introduzido, você precisará adicionar uma terceira coluna de chave estrangeira anulável à tabela.
precisa saber é o seguinte
@ Shadoninja: Você não está errado. Na verdade, acho que é uma maneira completamente justa de expressar isso. Geralmente, eu estou bem com esse tipo de solução, onde ela é justificada, mas certamente não seria a primeira em minha mente ao considerar as opções - precisamente pelo motivo que você descreveu.
Andriy M
2
@ Frank.Germain Nesse caso, você pode usar uma chave estrangeira exclusiva com base em duas colunas RefID, RefTypeonde RefTypeé um identificador fixo da tabela de destino. Se você precisar de integridade, poderá fazer verificações na camada de gatilho ou aplicativo. A recuperação genérica é possível neste caso. O SQL deve permitir uma definição de FK assim, facilitando nossas vidas.
djmj
2

Ainda outra opção é ter, em Ticket, uma coluna especificando o tipo de entidade proprietária ( Userou Group), a segunda coluna com referência Userou Groupid e NÃO usar chaves estrangeiras, mas confiar em um gatilho para impor a integridade referencial.

Duas vantagens que vejo aqui sobre o excelente modelo de Nathan (acima):

  • Clareza e simplicidade mais imediatas.
  • Consultas mais simples de escrever.
Jan Żankowski
fonte
1
Mas isso não permitiria uma chave estrangeira, certo? Eu ainda estou tentando descobrir o projeto certo para o meu projeto atual, em que uma tabela pode fazer referência a pelo menos 3 talvez mais no futuro
Can Rau
2

Outra abordagem é criar uma tabela de associação que contenha colunas para cada tipo de recurso em potencial. No seu exemplo, cada um dos dois tipos de proprietários existentes possui sua própria tabela (o que significa que você tem algo a referenciar). Se este for sempre o caso, você pode ter algo parecido com isto:

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
)

Com esta solução, você continuaria adicionando novas colunas ao adicionar novas entidades ao banco de dados e excluir e recriar o padrão de restrição de chave estrangeira mostrado por @Nathan Skerl. Esta solução é muito semelhante à @ Nathan Skerl, mas parece diferente (de acordo com a preferência).

Se você não tiver uma nova tabela para cada novo tipo de proprietário, talvez seja bom incluir um owner_type em vez de uma coluna de chave estrangeira para cada potencial proprietário:

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
)

Com o método acima, você pode adicionar quantos tipos de proprietários desejar. O Owner_ID não teria uma restrição de chave estrangeira, mas seria usado como uma referência para as outras tabelas. A desvantagem é que você teria que olhar para a tabela para ver o que o proprietário digita, pois não é imediatamente óbvio com base no esquema. Eu sugeriria isso apenas se você não conhecer os tipos de proprietário com antecedência e eles não estarão vinculados a outras tabelas. Se você conhece os tipos de proprietários de antemão, eu usaria uma solução como @ Nathan Skerl.

Desculpe se eu entendi errado o SQL, acabei de jogar isso juntos.

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

Eu acho que seria a maneira mais geral de representar o que você deseja, em vez de usar uma bandeira.

Francisco Soto
fonte