Clave externa para varias tablas

127

Tengo 3 tablas relevantes en mi base de datos.

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
)

Los usuarios pertenecen a múltiples grupos. Esto se hace a través de una relación de muchos a muchos, pero irrelevante en este caso. Un boleto puede ser propiedad de un grupo o un usuario, a través del campo dbo.Ticket.Owner.

¿Cuál sería la forma MÁS CORRECTA de describir esta relación entre un ticket y, opcionalmente, un usuario o un grupo?

Estoy pensando que debería agregar una bandera en la tabla de tickets que diga qué tipo es el propietario.

Darthg8r
fuente
En mi opinión, cada boleto es propiedad de un grupo. Es solo que un usuario es un grupo de uno. Qué opción 4 de los modelos @ nathan-skerl. Si usa Guids como teclas, todo funciona bastante bien
GraemeMiller

Respuestas:

149

Tiene algunas opciones, todas variadas en "corrección" y facilidad de uso. Como siempre, el diseño correcto depende de sus necesidades.

  • Simplemente puede crear dos columnas en Ticket, OwnedByUserId y OwnedByGroupId, y tener claves foráneas anulables para cada tabla.

  • Puede crear tablas de referencia M: M que habiliten las relaciones ticket: usuario y ticket: grupo. ¿Quizás en el futuro desee permitir que un solo boleto sea propiedad de múltiples usuarios o grupos? Este diseño no exige que un boleto sea ​​propiedad de una sola entidad solamente.

  • Puede crear un grupo predeterminado para cada usuario y tener tickets simplemente poseídos por un Grupo verdadero o un Grupo predeterminado del Usuario.

  • O (mi elección) modelar una entidad que actúa como base tanto para usuarios como para grupos, y tener tickets propiedad de esa entidad.

Aquí hay un ejemplo aproximado usando su 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
fuente
77
¿Cómo sería una consulta para tickets de usuario / grupo? Gracias.
paulkon
44
¿Cuál es el beneficio de las columnas calculadas persistentes en las tablas de Grupo y Usuario? La clave principal en la tabla Party ya garantiza que no habrá superposición en ID de grupo e ID de usuario, por lo que la clave externa solo debe estar en PartyId solo. Cualquier consulta escrita aún necesitaría conocer las tablas de PartyTypeName de todos modos.
Arin Taylor
1
@ArinTaylor, la columna persistente nos impide crear una Parte de tipo Usuario y relacionarla con un registro en dbo.Group.
Nathan Skerl
3
@paulkon Sé que esta es una pregunta antigua, pero la consulta sería algo así. 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;En el resultado, tendría cada asunto de boleto y nombre de propietario.
Corey McMahon
2
Con respecto a la opción 4, ¿alguien puede confirmar si se trata de un anti patrón o una solución para un anti patrón?
inckka
31

La primera opción en la lista de @Nathan Skerl es lo que se implementó en un proyecto con el que trabajé una vez, donde se estableció una relación similar entre tres tablas. (Uno de ellos hizo referencia a otros dos, uno a la vez).

Entonces, la tabla de referencia tenía dos columnas de clave externa, y también tenía una restricción para garantizar que una sola fila hiciera referencia a exactamente una tabla (no ambas, ni ninguna).

Así es como podría verse cuando se aplica a sus tablas:

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 puede ver, la Tickettabla tiene dos columnas OwnerGroupy OwnerUserambas son claves foráneas que pueden contener nulos. (Las columnas respectivas en las otras dos tablas se convierten en claves primarias en consecuencia.) La CK_Ticket_GroupUserrestricción de verificación asegura que solo una de las dos columnas de clave externa contenga una referencia (la otra es NULL, por eso ambas deben ser anulables).

(La clave principal Ticket.IDno es necesaria para esta implementación en particular, pero definitivamente no estaría de más tener una en una tabla como esta).

Andriy M
fuente
1
Esto también es lo que tenemos en nuestro software y evitaría si está tratando de crear un marco genérico de acceso a datos. Este diseño aumentará la complejidad en la capa de la aplicación.
Frank.Germain
44
Soy realmente nuevo en SQL, así que corrígeme si esto está mal, pero este diseño parece ser un enfoque para usar cuando estás extremadamente seguro de que solo necesitarás dos tipos de tickets de propietario. En el futuro, si se introdujera un tercer tipo de propietario de ticket, tendría que agregar una tercera columna de clave externa anulable a la tabla.
Shadoninja
@Shadoninja: No te equivocas. De hecho, creo que es una forma completamente justa de decirlo. En general, estoy de acuerdo con este tipo de solución donde está justificada, pero ciertamente no sería lo primero en mi mente al considerar las opciones, precisamente por la razón que ha descrito.
Andriy M
2
@ Frank.Germain En este caso, puede usar una clave foránea única basada en dos columnas RefID, RefTypedonde RefTypehay un identificador fijo de la tabla de destino. Si necesita integridad, puede hacer verificaciones en el disparador o la capa de la aplicación. La recuperación genérica es posible en este caso. SQL debería permitir una definición de FK como esta, facilitando nuestras vidas.
djmj
2

Otra opción es tener, en Ticket, una columna que especifique el tipo de entidad propietaria ( Usero Group), la segunda columna con referencia Usero Groupid y NO usar claves externas, sino confiar en un activador para hacer cumplir la integridad referencial.

Dos ventajas que veo aquí sobre el excelente modelo de Nathan (arriba):

  • Más claridad inmediata y simplicidad.
  • Consultas más simples para escribir.
Jan Żankowski
fuente
1
Pero esto no permitiría una clave externa ¿verdad? Todavía estoy tratando de encontrar el diseño correcto para mi proyecto actual, donde una tabla puede hacer referencia al menos a 3 o más en el futuro
Can Rau
2

Otro enfoque es crear una tabla de asociación que contenga columnas para cada tipo de recurso potencial. En su ejemplo, cada uno de los dos tipos de propietarios existentes tiene su propia tabla (lo que significa que tiene algo a lo que hacer referencia). Si este siempre será el caso, puede tener algo como esto:

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 esta solución, continuaría agregando nuevas columnas a medida que agrega nuevas entidades a la base de datos y eliminaría y volvería a crear el patrón de restricción de clave externa que muestra @Nathan Skerl. Esta solución es muy similar a @Nathan Skerl, pero se ve diferente (hasta la preferencia).

Si no va a tener una nueva Tabla para cada nuevo tipo de Propietario, entonces sería bueno incluir un tipo de propietario en lugar de una columna de clave externa para cada Propietario potencial:

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 el método anterior, puede agregar tantos tipos de propietarios como desee. Owner_ID no tendría una restricción de clave externa, pero se utilizaría como referencia para las otras tablas. La desventaja es que tendrías que mirar la tabla para ver cuáles son los tipos de propietarios, ya que no es inmediatamente obvio según el esquema. Solo sugeriría esto si no conoce los tipos de propietario de antemano y no se vincularán a otras tablas. Si conoce de antemano los tipos de propietarios, elegiría una solución como @Nathan Skerl.

Lo siento si me equivoqué con un poco de SQL, solo hice esto.

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

Creo que esa sería la forma más general de representar lo que quieres en lugar de usar una bandera.

Francisco Soto
fuente