¿Cómo modelar un tipo de entidad que puede tener diferentes conjuntos de atributos?

11

Tengo algunos problemas para recrear una base de datos con una relación uno a muchos (1: M) entre usuarios y elementos .

Esto es bastante sencillo, sí; sin embargo, cada artículo pertenece a una determinada categoría (por ejemplo, un automóvil , un bote o un avión ), y cada categoría tiene un número particular de atributos, por ejemplo:

Car estructura:

+----+--------------+--------------+
| PK | Attribute #1 | Attribute #2 |
+----+--------------+--------------+

Boat estructura:

+----+--------------+--------------+--------------+
| PK | Attribute #1 | Attribute #2 | Attribute #3 |
+----+--------------+--------------+--------------+

Plane estructura:

+----+--------------+--------------+--------------+--------------+
| PK | Attribute #1 | Attribute #2 | Attribute #3 | Attribute #4 |
+----+--------------+--------------+--------------+--------------+

Debido a esta diversidad en el número de atributos (columnas), inicialmente pensé que sería una buena idea crear una tabla separada para cada Categoría , por lo que evitaría varios NULL y, por lo tanto, haría un mejor uso de la indexación.

Aunque al principio se veía genial, no pude encontrar una manera de crear la relación entre los Artículos y las Categorías a través de la base de datos porque, al menos en mi modesta experiencia como administrador de la base de datos, al crear claves externas, informo explícitamente una base de datos El nombre de la tabla y la columna.

Al final, me gustaría una estructura sólida para almacenar todos los datos, al tiempo que tengo todos los medios para enumerar todos los atributos de todos los elementos que un usuario puede tener con una consulta.

Podría codificar consultas dinámicas con el lenguaje del lado del servidor , pero siento que esto está mal y no es muy óptimo.

Información Adicional

Estas son mis respuestas a los comentarios de MDCCL:

1. ¿Cuántas categorías de artículos de interés hay en su contexto comercial, tres (es decir, automóviles , barcos y aviones ) o más?

De hecho, es muy simple: solo hay cinco categorías en total.

2. ¿El mismo Artículo siempre pertenecerá al mismo Usuario (es decir, una vez que un Artículo dado ha sido "asignado" a un Usuario determinado , no se puede cambiar)?

No, podrían cambiar. En el escenario ficticio de la pregunta, sería como si el Usuario A vendiera el Artículo # 1 para el Usuario B , por lo que la propiedad debe reflejarse.

3. ¿Hay atributos compartidos por algunas o todas las Categorías ?

No se comparte pero, de memoria, puedo decir que al menos tres atributos están presentes en todas las categorías .

4. ¿Existe la posibilidad de que la cardinalidad de la relación entre Usuario y Elemento sea ​​de muchos a muchos (M: N) en lugar de uno a muchos (1: M)? Por ejemplo, en el caso de las siguientes reglas comerciales: A User owns zero-one-or-many ItemsyAn Item is owned by one-to-many Users

No, porque los Artículos describirían un objeto físico. Los usuarios tendrán una copia virtual de ellos, cada uno identificado por un GUID único v4

5. Con respecto a su siguiente respuesta a uno de los comentarios de la pregunta:

"En el escenario ficticio de la pregunta, sería como si el Usuario A vendiera el Artículo # 1 para el Usuario B , por lo que la propiedad debe reflejarse".

Parece que está planeando realizar un seguimiento de la evolución de la propiedad del artículo , por así decirlo. De esta manera, ¿qué atributos te gustaría almacenar sobre este fenómeno? ¿Solo la modificación del atributo que indica el Usuario específico que es el Propietario de un Artículo específico ?

No en realidad no. La propiedad puede cambiar, pero no necesito hacer un seguimiento del propietario anterior .

usuario5613506
fuente

Respuestas:

18

De acuerdo con su descripción del entorno empresarial en consideración, existe una estructura de subtipo de supertipo que abarca el elemento , el supertipo, y cada una de sus categorías , es decir, automóvil , barco y avión (junto con dos más que no se dieron a conocer): los subtipos

A continuación detallaré el método que seguiría para administrar tal escenario.

Reglas del negocio

Para comenzar a delinear el esquema conceptual relevante , algunas de las reglas comerciales más importantes determinadas hasta ahora (restringiendo el análisis solo a las tres Categorías divulgadas , para mantener las cosas lo más breve posible) pueden formularse de la siguiente manera:

  • Un usuario posee cero uno o muchos artículos
  • Un artículo es propiedad de exactamente un usuario en un instante específico
  • Un artículo puede ser propiedad de uno a muchos usuarios en distintos momentos
  • Un artículo se clasifica por exactamente una categoría
  • Un artículo es, en todo momento,
    • ya sea un auto
    • o un barco
    • o un avión

Diagrama ilustrativo de IDEF1X

La Figura 1 muestra un diagrama IDEF1X 1 que creé para agrupar las formulaciones anteriores junto con otras reglas comerciales que parecen pertinentes:

Figura 1 - Estructura de Supertipo-Subtipo de Artículo y Categorías

Supertipo

Por un lado, Item , el supertipo, presenta las propiedades o atributos que son comunes a todas las Categorías , es decir,

  • CategoryCode —especificado como una LLAVE EXTRANJERA (FK) que hace referencia a Category.CategoryCode y funciona como un discriminador de subtipo , es decir, indica la categoría exacta de subtipo con la que se debe conectar un elemento dado—,
  • OwnerId, que se distingue como un FK que apunta a User.UserId , pero le asigné un nombre de rol 2 para reflejar sus implicaciones especiales con mayor precisión,
  • foo ,
  • Bar ,
  • Baz y
  • CreatedDateTime .

Subtipos

Por otro lado, las propiedades que pertenecen a cada categoría en particular , es decir,

  • Qux y Corge ;
  • Grault , Garply y Plugh ;
  • Xyzzy , Thud , Wibble y Flob ;

se muestran en el cuadro de subtipo correspondiente.

Identificadores

Luego, Item.ItemId PRIMARY KEY (PK) ha migrado 3 a los subtipos con diferentes nombres de roles, es decir,

  • CarId ,
  • BoatId y
  • PlaneId .

Asociaciones mutuamente excluyentes

Como se muestra, existe una asociación o relación de cardinalidad uno a uno (1: 1) entre (a) cada supertipo y (b) su instancia de subtipo complementario.

El símbolo de subtipo exclusivo retrata el hecho de que los subtipos son mutuamente excluyentes, es decir, la aparición de un Elemento concreto puede complementarse con una sola instancia de subtipo solo: un Coche , o un Avión , o un Barco (nunca por dos o más).

, Empleé nombres de marcador de posición clásicos para titular algunas de las propiedades de tipo de entidad, ya que sus denominaciones reales no se proporcionaron en la pregunta.

Diseño de nivel lógico expositivo

En consecuencia, para analizar un diseño lógico expositivo, deduje las siguientes instrucciones SQL-DDL basadas en el diagrama IDEF1X mostrado y descrito anteriormente:

-- You should determine which are the most fitting 
-- data types and sizes for all your table columns 
-- depending on your business context characteristics.

-- Also, you should make accurate tests to define the 
-- most convenient INDEX strategies based on the exact 
-- data manipulation tendencies of your business context.

-- As one would expect, you are free to utilize 
-- your preferred (or required) naming conventions. 

CREATE TABLE UserProfile (
    UserId          INT      NOT NULL,
    FirstName       CHAR(30) NOT NULL,
    LastName        CHAR(30) NOT NULL,
    BirthDate       DATE     NOT NULL,
    GenderCode      CHAR(3)  NOT NULL,
    Username        CHAR(20) NOT NULL,
    CreatedDateTime DATETIME NOT NULL,
    --
    CONSTRAINT UserProfile_PK  PRIMARY KEY (UserId),
    CONSTRAINT UserProfile_AK1 UNIQUE ( -- Composite ALTERNATE KEY.
        FirstName,
        LastName,
        GenderCode,
        BirthDate
    ),
    CONSTRAINT UserProfile_AK2 UNIQUE (Username) -- ALTERNATE KEY.
);

CREATE TABLE Category (
    CategoryCode     CHAR(1)  NOT NULL, -- Meant to contain meaningful, short and stable values, e.g.; 'C' for 'Car'; 'B' for 'Boat'; 'P' for 'Plane'.
    Name             CHAR(30) NOT NULL,
    --
    CONSTRAINT Category_PK PRIMARY KEY (CategoryCode),
    CONSTRAINT Category_AK UNIQUE      (Name) -- ALTERNATE KEY.
);

CREATE TABLE Item ( -- Stands for the supertype.
    ItemId           INT      NOT NULL,
    OwnerId          INT      NOT NULL,
    CategoryCode     CHAR(1)  NOT NULL, -- Denotes the subtype discriminator.
    Foo              CHAR(30) NOT NULL,
    Bar              CHAR(30) NOT NULL,
    Baz              CHAR(30) NOT NULL,  
    CreatedDateTime  DATETIME NOT NULL,
    --
    CONSTRAINT Item_PK             PRIMARY KEY (ItemId),
    CONSTRAINT Item_to_Category_FK FOREIGN KEY (CategoryCode)
        REFERENCES Category    (CategoryCode),
    CONSTRAINT Item_to_User_FK     FOREIGN KEY (OwnerId)
        REFERENCES UserProfile (UserId)  
);

CREATE TABLE Car ( -- Represents one of the subtypes.
    CarId INT      NOT NULL, -- Must be constrained as (a) the PRIMARY KEY and (b) a FOREIGN KEY.
    Qux   CHAR(30) NOT NULL,
    Corge CHAR(30) NOT NULL,   
    --
    CONSTRAINT Car_PK         PRIMARY KEY (CarId),
    CONSTRAINT Car_to_Item_FK FOREIGN KEY (CarId)
        REFERENCES Item (ItemId)  
);

CREATE TABLE Boat ( -- Stands for one of the subtypes.
    BoatId INT      NOT NULL, -- Must be constrained as (a) the PRIMARY KEY and (b) a FOREIGN KEY.
    Grault CHAR(30) NOT NULL,
    Garply CHAR(30) NOT NULL,   
    Plugh  CHAR(30) NOT NULL, 
    --
    CONSTRAINT Boat_PK         PRIMARY KEY (BoatId),
    CONSTRAINT Boat_to_Item_FK FOREIGN KEY (BoatId)
        REFERENCES Item (ItemId)  
);

CREATE TABLE Plane ( -- Denotes one of the subtypes.
    PlaneId INT      NOT NULL, -- Must be constrained as (a) the PRIMARY KEY and (b) a FOREIGN KEY.
    Xyzzy   CHAR(30) NOT NULL,
    Thud    CHAR(30) NOT NULL,  
    Wibble  CHAR(30) NOT NULL,
    Flob    CHAR(30) NOT NULL,  
    --
    CONSTRAINT Plane_PK         PRIMARY KEY (PlaneId),
    CONSTRAINT Plane_to_Item_PK FOREIGN KEY (PlaneId)
        REFERENCES Item (ItemId)  
);

Como se demostró, el tipo de superentidad y cada uno de los tipos de subentidad están representados por la tabla base correspondiente .

Las columnas CarId, BoatIdy PlaneId, con restricciones como las PK de las tablas adecuadas, ayuda en la representación del nivel conceptual uno-a-uno asociación por medio de restricciones FK § ese punto de la ItemIdcolumna, que está limitado como el PK de la Itemtabla. Esto significa que, en un "par" real, tanto las filas de supertipo como de subtipo están identificadas por el mismo valor PK; Por lo tanto, es más que oportuno mencionar que

  • (a) adjuntar una columna adicional para contener los valores sustitutos controlados por el sistema a (b) las tablas que representan los subtipos es (c) completamente superfluo .

§ Para evitar problemas y errores relacionados con las definiciones de restricciones CLAVE (particularmente EXTRANJERAS), situación a la que se refirió en los comentarios, es muy importante tener en cuenta la dependencia de la existencia que tiene lugar entre las diferentes tablas disponibles, como se ejemplifica en el orden de declaración de las tablas en la estructura DDL expositiva, que también proporcioné en este SQL Fiddle .

Por ejemplo, agregar una columna adicional con la propiedad AUTO_INCREMENT a una tabla de una base de datos construida en MySQL.

Consideraciones de integridad y consistencia

Es fundamental señalar que, en su entorno empresarial, debe (1) asegurarse de que cada fila de "supertipo" esté en todo momento complementada por su correspondiente contraparte "subtipo" y, a su vez, (2) garantizar que dicho La fila "subtipo" es compatible con el valor contenido en la columna "discriminador" de la fila "supertipo".

Sería muy elegante hacer cumplir tales circunstancias de manera declarativa , pero, desafortunadamente, ninguna de las principales plataformas SQL ha proporcionado los mecanismos adecuados para hacerlo, por lo que sé. Por lo tanto, recurrir al código de procedimiento dentro de ACID TRANSACTIONS es bastante conveniente para que estas condiciones siempre se cumplan en su base de datos. Otra opción sería emplear DISPARADORES, pero tienden a desordenar las cosas, por así decirlo.

Declarando vistas útiles

Con un diseño lógico como el explicado anteriormente, sería muy práctico crear una o más vistas, es decir, tablas derivadas que comprendan columnas que pertenezcan a dos o más de las tablas base relevantes . De esta manera, puede, por ejemplo, SELECCIONAR directamente desde esas vistas sin tener que escribir todas las UNIONES cada vez que tenga que recuperar información "combinada".

Data de muestra

A este respecto, digamos que las tablas base están "pobladas" con los datos de muestra que se muestran a continuación:

--

INSERT INTO UserProfile 
    (UserId, FirstName, LastName, BirthDate, GenderCode, Username, CreatedDateTime)
VALUES
    (1, 'Edgar', 'Codd', '1923-08-19', 'M', 'ted.codd', CURDATE()),
    (2, 'Michelangelo', 'Buonarroti', '1475-03-06', 'M', 'michelangelo', CURDATE()),
    (3, 'Diego', 'Velázquez', '1599-06-06', 'M', 'd.velazquez', CURDATE());

INSERT INTO Category 
    (CategoryCode, Name)
VALUES
    ('C', 'Car'), ('B', 'Boat'), ('P', 'Plane');

-- 1. ‘Full’ Car INSERTion

-- 1.1 
INSERT INTO Item
    (ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
    (1, 1, 'C', 'This datum', 'That datum', 'Other datum', CURDATE());

 -- 1.2
INSERT INTO Car
    (CarId, Qux, Corge)
VALUES
    (1, 'Fantastic Car', 'Powerful engine pre-update!');

-- 2. ‘Full’ Boat INSERTion

-- 2.1
INSERT INTO Item
  (ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
  (2, 2, 'B', 'This datum', 'That datum', 'Other datum', CURDATE());

-- 2.2
INSERT INTO Boat
    (BoatId, Grault, Garply, Plugh)
VALUES
    (2, 'Excellent boat', 'Use it to sail', 'Everyday!');

-- 3 ‘Full’ Plane INSERTion

-- 3.1
INSERT INTO Item
  (ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
  (3, 3, 'P', 'This datum', 'That datum', 'Other datum', CURDATE());

-- 3.2
INSERT INTO Plane
    (PlaneId, Xyzzy, Thud, Wibble, Flob)
VALUES
    (3, 'Extraordinary plane', 'Traverses the sky', 'Free', 'Like a bird!');

--

Entonces, una vista ventajoso es uno que reúne las columnas de Item, Cary UserProfile:

--

CREATE VIEW CarAndOwner AS
    SELECT C.CarId,
           I.Foo,
           I.Bar,
           I.Baz,
           C.Qux,
           C.Corge,           
           U.FirstName AS OwnerFirstName,
           U.LastName  AS OwnerLastName
        FROM Item I
        JOIN Car C
          ON C.CarId = I.ItemId
        JOIN UserProfile U
          ON U.UserId = I.OwnerId;

--

Naturalmente, se puede seguir un enfoque similar para que también pueda SELECCIONAR la información "completa" Boaty Planedirectamente desde una sola tabla (una derivada, en estos casos).

Después de que -si no me importa acerca de la presencia de marcas NULL en consecuencia Conjuntos- con la siguiente definición de vista, se puede, por ejemplo, “recoger” las columnas de las tablas Item, Car, Boat, Planey UserProfile:

--

CREATE VIEW FullItemAndOwner AS
    SELECT I.ItemId,
           I.Foo, -- Common to all Categories.
           I.Bar, -- Common to all Categories.
           I.Baz, -- Common to all Categories.
          IC.Name      AS Category,
           C.Qux,    -- Applies to Cars only.
           C.Corge,  -- Applies to Cars only.
           --
           B.Grault, -- Applies to Boats only.
           B.Garply, -- Applies to Boats only.
           B.Plugh,  -- Applies to Boats only.
           --
           P.Xyzzy,  -- Applies to Planes only.
           P.Thud,   -- Applies to Planes only.
           P.Wibble, -- Applies to Planes only.
           P.Flob,   -- Applies to Planes only.
           U.FirstName AS OwnerFirstName,
           U.LastName  AS OwnerLastName
        FROM Item I
        JOIN Category IC
          ON I.CategoryCode = IC.CategoryCode
   LEFT JOIN Car C
          ON C.CarId = I.ItemId
   LEFT JOIN Boat B
          ON B.BoatId = I.ItemId
   LEFT JOIN Plane P
          ON P.PlaneId = I.ItemId               
        JOIN UserProfile U
          ON U.UserId = I.OwnerId;

--

El código de las vistas aquí mostradas es solo ilustrativo. Por supuesto, hacer algunos ejercicios de prueba y modificaciones podría ayudar a acelerar la ejecución (física) de las consultas en cuestión. Además, es posible que deba eliminar o agregar columnas a dichas vistas según lo requiera la empresa.

Los datos de muestra y todas las definiciones de vista se incorporan a este Fiddle de SQL para que puedan observarse "en acción".

Manipulación de datos: código de programa (s) de aplicación y alias de columna

El uso del código de los programas de aplicación (si eso es lo que quiere decir con "código específico del lado del servidor") y los alias de columna son otros puntos importantes que mencionó en los siguientes comentarios:

  • Me las arreglé para solucionar el problema [unir] con código específico del lado del servidor, pero realmente no quiero hacer eso -Y- agregar alias a todas las columnas podría ser "estresante".

  • Muy bien explicado, muchas gracias. Sin embargo, como sospechaba, tendré que manipular el conjunto de resultados al enumerar todos los datos debido a las similitudes con algunas columnas, ya que no quiero usar varios alias para mantener limpia la declaración.

Es oportuno indicar que, si bien el uso del código del programa de aplicación es un recurso muy apropiado para manejar las características de presentación (o gráficas) de los conjuntos de resultados, es fundamental evitar la recuperación de datos fila por fila para evitar problemas de velocidad de ejecución. El objetivo debe ser "buscar" los conjuntos de datos pertinentes in toto por medio de los instrumentos de manipulación de datos robustos proporcionados por el motor de conjuntos (precisamente) de la plataforma SQL para que pueda optimizar el comportamiento de su sistema.

Además, el uso de alias para cambiar el nombre de una o más columnas dentro de un determinado alcance puede parecer estresante, pero, personalmente, veo ese recurso como una herramienta muy poderosa que ayuda a (i) contextualizar y (ii) desambiguar el significado y la intención atribuidos a lo concerniente columnas; por lo tanto, este es un aspecto que debe ser meditado a fondo con respecto a la manipulación de los datos de interés.

Escenarios similares

También podría encontrar ayuda en esta serie de publicaciones y este grupo de publicaciones que contienen mi opinión sobre otros dos casos que incluyen asociaciones de supertipo-subtipo con subtipos mutuamente excluyentes.

También he propuesto una solución para un entorno empresarial que involucra un clúster de supertipo-subtipo donde los subtipos no son mutuamente excluyentes en esta respuesta (más reciente) .


Notas finales

1 La definición de integración para el modelado de información ( IDEF1X ) es una técnica de modelado de datos altamente recomendable que fue establecida como estándar en diciembre de 1993 por el Instituto Nacional de Estándares y Tecnología de EE. UU. (NIST). Se basa sólidamente en (a) algunos de los trabajos teóricos escritos por el único autor del modelo relacional , es decir, el Dr. EF Codd ; en (b) la vista entidad-relación , desarrollada por el Dr. PP Chen ; y también en (c) la Técnica de diseño de base de datos lógica, creada por Robert G. Brown.

2 En IDEF1X, un nombre de rol es una etiqueta distintiva asignada a una propiedad FK (o atributo) para expresar el significado que tiene dentro del alcance de su tipo de entidad respectivo.

3 El estándar IDEF1X define la migración de claves como "El proceso de modelado de colocar la clave primaria de una entidad primaria o genérica en su entidad secundaria o de categoría como clave externa".

MDCCL
fuente
1
No estoy seguro de entender su consulta, pero como se ilustra en el diseño DDL, la Itemtabla incluye una CategoryCodecolumna. Como se menciona en la sección titulada “Consideraciones de integridad y consistencia”:
MDCCL
1
Es fundamental señalar que, en su entorno empresarial, debe (1) asegurarse de que cada fila de "supertipo" se complementa en todo momento con su correspondiente contraparte de "subtipo" y, a su vez, (2) garantizar que dicho La fila "subtipo" es compatible con el valor contenido en la columna "discriminador" de la fila "supertipo".
MDCCL
1
Sería muy elegante hacer cumplir tales circunstancias de manera declarativa, pero, desafortunadamente, ninguna de las principales plataformas SQL ha proporcionado los mecanismos adecuados para hacerlo, por lo que sé. Por lo tanto, recurrir al código de procedimiento dentro de ACID TRANSACTIONS es bastante conveniente para que estas condiciones siempre se cumplan en su base de datos. Otra opción sería emplear DISPARADORES, pero tienden a desordenar las cosas, por así decirlo.
MDCCL
1
El quid de la cuestión es que ninguna implementación de SQL (incluido el dialecto MySQL) proporciona el soporte adecuado para ASSERTIONS, herramientas declarativas potentes y elegantes que ayudarían a evitar recurrir a enfoques de procedimiento (TRANSACTIONS o TRIGGERS), o trabajar de manera redundante como , por ejemplo, repitiendo innecesariamente las CategoryColumntablas que representan los subtipos (con todas las implicaciones en los niveles lógicos [por ejemplo, anomalías de modificación] y físicos de abstracción [por ejemplo, índices adicionales, estructuras más grandes, etc.]).
MDCCL
2
Hasta que cualquiera de los vendedores / desarrolladores del sistema de gestión de bases de datos proporcione ASERCIONES —la herramienta adecuada para esta tarea—, prefiero (a) los enfoques de procedimiento —ya sean TRANSACCIONES o ACTIVADORES— sobre (b) el curso de acción redundante, aunque (b) es una posibilidad, que personalmente no recomiendo. Por supuesto, el DBA debe administrar cuidadosamente los permisos con respecto a las operaciones válidas de manipulación de datos que se pueden ejecutar en la base de datos de relevancia, lo que definitivamente ayuda bastante en el mantenimiento de la integridad de los datos.
MDCCL
0

Llamemos a la mesa principal Productos. Esto aloja los atributos compartidos. Entonces, digamos que tenemos una mesa de coche, una mesa de avión y una mesa de barco. Estas tres tablas tendrían una clave ProductID con una restricción FK en la fila ID de la tabla Product. Si los quieres a todos, únete a ellos. Si solo quieres los autos, únete a la izquierda Cars con productos (o une a la derecha los productos y autos, pero prefiero usar siempre las combinaciones del lado izquierdo).

Esto se llama un modelo de datos jerárquico. Para un bajo número de sub-tablas, podría tener sentido en una tabla larga (millones de productos).

neManiac
fuente
¿Y luego me uno a los usuarios con productos?
user5613506
1
Por lo general, no necesita información del usuario cuando devuelve una lista de productos a su interfaz, necesita la información del producto. No tiene sentido unirse a Usuarios y Productos y devolver la información de usuario idéntica para cada fila de productos devueltos. Entonces, primero filtra por tipo de producto uniendo la tabla de productos y la subtabla apropiada (Auto, Barco ...) y luego filtra por Usuario usando una cláusula WHERE. En general, desearía tener un OwnerID en la tabla Productos (un FK en la columna ID de la tabla Usuario). Por lo tanto, agregaría un WHERE Owner = [Request.User].
neManiac