web-dev-qa-db-fra.com

Comment modéliser un type d'entité qui peut avoir différents ensembles d'attributs?

J'ai du mal à recréer une base de données avec une relation un-à-plusieurs (1: M) entre Users et Items.

C'est assez simple, oui; cependant, chaque Article appartient à une certaine Catégorie (par exemple, a Voiture, a Bateau ou a Avion), et chaque Catégorie a un nombre particulier d'attributs, par exemple:

Car structure:

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

Boat structure:

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

Plane structure:

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

En raison de cette diversité dans le nombre d'attributs (colonnes), j'ai d'abord pensé que ce serait une bonne idée de créer une table distincte pour chaque Category, donc j'éviterais plusieurs NULLs et ainsi mieux utiliser l'indexation.

Bien que cela paraisse bien au début, je n'ai pas trouvé de moyen de créer la relation entre les Items et les Categories via la base de données car, au moins dans ma modeste expérience en tant qu'administrateur de base de données, lors de la création de clés étrangères, j'informe explicitement une base de données du nom et de la colonne de la table.

Au final, je voudrais une structure solide pour stocker toutes les données, tout en ayant tous les moyens de lister tous les attributs de tous Items a User peut avoir avec une requête.

Je pourrais hardcode dynamic queries avec le langage côté serveur, mais je pense que c'est faux et pas très optimal.

Informations supplémentaires

Voici mes réponses aux commentaires de MDCCL:

1. Combien de Catégories d'articles présentant un intérêt dans votre contexte commercial, trois (c.-à-d. Voitures, Bateaux et Avions) ou plus?

En fait, c'est très simple: il n'y a que cinq Catégories au total.

2. Est-ce que le même Article appartiendra toujours au même Utilisateur (que est, une fois qu'un article donné a été "attribué" à un certain utilisateur il ne peut pas être modifié)?

Non, ils pourraient changer. Dans le scénario fictif de la question, ce serait comme L'utilisateur A vend l'article n ° 1 à l'utilisateur B, donc la propriété doit être reflétée.

3. Y a-t-il des attributs qui sont partagés par tout ou partie des Catégories?

Non partagé mais, de mémoire, je peux dire qu'au moins trois attributs sont présents dans tous les Catégories.

4. Y a-t-il une chance que la cardinalité de la relation entre Utilisateur et Article est plusieurs-à-plusieurs (M: N) au lieu d'un-à-plusieurs (1: M)? Par exemple, dans le cas des règles métier suivantes: A User owns zero-one-or-many Items et An Item is owned by one-to-many Users

Non, car Items décrirait un objet physique. Utilisateurs en aura une copie virtuelle, chacun identifié par un unique GUID v4

5. Concernant votre réponse suivante à l'un des commentaires de la question:

"Dans le scénario fictif de la question, ce serait comme L'utilisateur A vend l'article n ° 1 à l'utilisateur B, donc la propriété doit être reflétée."

Il semble que vous envisagiez de suivre l'évolution de la propriété des objets, pour ainsi dire. De cette façon, quels attributs aimeriez-vous stocker sur un tel phénomène? Seule la modification de l'attribut qui indique le Utilisateur spécifique qui est le Propriétaire d'un Article?

Non, pas vraiment. Le propriété peut changer, mais je n'ai pas besoin de garder une trace du précédent Propriétaire.

11
user5613506

Selon votre description de l'environnement commercial considéré, il existe une structure de sous-type supertype qui englobe l'élément —le supertype— et chacun de ses Catégories , c.-à-d. Voiture , Bateau et Avion (avec avec deux autres qui n'ont pas été connus) - les sous-types -.

Je détaillerai ci-dessous la méthode que je suivrais pour gérer un tel scénario.

Règles métier

Afin de commencer à délimiter le schéma conceptuel pertinent , certaines des règles métier les plus importantes ont été déterminées jusqu'à présent (en limitant l'analyse aux trois divulguées Les catégories uniquement, pour être aussi bref que possible) peuvent être formulées comme suit:

  • A User owns zero-one-or-many Items
  • Un élément appartient à exactement un utilisateur à un instant spécifique
  • Un élément peut appartenir à un à plusieurs utilisateurs à des moments différents dans le temps
  • Un élément est classé par exactement une catégorie
  • Un élément est, à tout moment,
    • soit une voiture
    • ou un bateau
    • ou un avion

Diagramme illustratif IDEF1X

Figure 1 affiche un IDEF1X 1  diagramme que j'ai créé pour regrouper les formulations précédentes ainsi que d'autres règles métier qui semblent pertinentes:

Figure 1 - Item and Categories Supertype-Subtype Structure

Supertype

D'une part, Item , le supertype, présente les propriétés ou des attributs communs à toutes les catégories , c'est-à-dire,

  • CategoryCode - spécifié comme une clé étrangère (FK) qui référence Category.CategoryCode et fonctionne comme un discriminateur de sous-type , c'est-à-dire qu'il indique la catégorie exacte de sous-type avec laquelle un élément donné doit être connecté—,
  • OwnerId - Distingué comme un FK qui pointe vers User.UserId , mais je lui ai attribué un nom de rôle 2  afin de refléter plus précisément ses implications particulières -,
  • Foo ,
  • Barre ,
  • Baz et
  • CreatedDateTime .

Sous-types

D'un autre côté, les propriétés qui se rapportent à chaque catégorie particulière , c'est-à-dire,

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

sont affichés dans la boîte de sous-type correspondante.

Identifiants

Ensuite, la Item.ItemId PRIMARY KEY (PK) a migré 3  aux sous-types avec des noms de rôle différents, c'est-à-dire,

  • CarId ,
  • BoatId et
  • PlaneId .

Associations mutuellement exclusives

Comme illustré, il existe une association ou une relation de cardinalité biunivoque (1: 1) entre (a) chaque occurrence de supertype et (b) son instance de sous-type complémentaire.

Le symbole de sous-type exclusif décrit le fait que les sous-types s'excluent mutuellement, c'est-à-dire qu'une occurrence concrète d'élément peut être complétée par une seule instance de sous-type uniquement: soit une voiture , soit un avion , soit un bateau (jamais par deux ou plus).

,  J'ai utilisé des noms d'espace réservé classiques pour autoriser certaines propriétés de type d'entité, car leurs dénominations réelles n'ont pas été fournies dans la question.

Disposition de niveau logique expositoire

Par conséquent, afin de discuter d'une conception logique expositoire, j'ai dérivé les instructions SQL-DDL suivantes basées sur le diagramme IDEF1X affiché et décrit ci-dessus:

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

Comme démontré, le type de superentité et chacun des types de sous-entité sont représentés par la table de base correspondante .

Les colonnes CarId, BoatId et PlaneId, contraintes en tant que PK des tables appropriées, aident à représenter l'association biunivoque de niveau conceptuel au moyen de contraintes FK§ qui pointent vers la colonne ItemId, qui est contrainte en tant que PK de la table Item. Cela signifie que, dans une "paire" réelle, les lignes de supertype et de sous-type sont identifiées par la même valeur PK; il est donc plus que opportun de mentionner que

  • (a) attacher une colonne supplémentaire pour contenir les valeurs de substitution contrôlées par le système pour (b) les tableaux représentant les sous-types est (c) entièrement superflu .

§ Afin d'éviter les problèmes et les erreurs concernant (en particulier les définitions de contraintes KEY ÉTRANGÈRES - situation que vous avez mentionnée dans les commentaires), il est très important de prendre en compte la dépendance à l'existence qui se produit les différentes tables à portée de main, comme illustré dans l'ordre de déclaration des tables dans la structure DDL expositive, que j'ai fournie dans ce SQL Fiddle aussi.

 Par exemple, en ajoutant une colonne supplémentaire avec la propriété AUTO_INCREMENT à une table d'une base de données construite sur MySQL.

Considérations d'intégrité et de cohérence

Il est essentiel de souligner que, dans votre environnement commercial, vous devez (1) vous assurer que chaque ligne de "supertype" est à tout moment complétée par son homologue "sous-type" correspondante et, à son tour, (2) garantir que ladite La ligne "sous-type" est compatible avec la valeur contenue dans la colonne "discriminateur" de la ligne "supertype".

Il serait très élégant d'appliquer de telles circonstances de manière déclarative mais, malheureusement, aucune des principales plates-formes SQL n'a fourni les mécanismes appropriés pour le faire, à ma connaissance. Par conséquent, le recours au code procédural dans ACID TRANSACTIONS est assez pratique pour que ces conditions soient toujours remplies dans votre base de données. Une autre option consisterait à utiliser TRIGGERS, mais ils ont tendance à rendre les choses désordonnées, pour ainsi dire.

Déclarer des vues utiles

Ayant une conception logique comme celle expliquée ci-dessus, il serait très pratique de créer une ou plusieurs vues, c'est-à-dire, des tables dérivées qui comprennent des colonnes qui appartiennent à deux ou plusieurs des tables de base . De cette façon, vous pouvez, par exemple, sélectionner directement à partir de ces vues sans avoir à écrire toutes les jointures à chaque fois que vous devez récupérer des informations "combinées".

Exemples de données

À cet égard, disons que les tables de base sont "remplies" avec les exemples de données ci-dessous:

--

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

--

Ensuite, une vue avantageuse est celle qui rassemble les colonnes de Item, Car et 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;

--

Naturellement, une approche similaire peut être suivie afin que vous puissiez également sélectionner les informations "complètes" Boat et Plane directement depuis une seule table (a dérivé, dans ces cas).

Après cela - si cela ne vous dérange pas de la présence de marques NULL dans les jeux de résultats - avec la définition VIEW suivante, vous pouvez, par exemple, "collecter" des colonnes des tables Item, Car, Boat, Plane et 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;

--

Le code des vues présentées ici n'est qu'illustratif. Bien sûr, faire des tests et des modifications peut aider à accélérer l'exécution (physique) des requêtes en cours. En outre, vous devrez peut-être supprimer ou ajouter des colonnes auxdites vues en fonction des besoins de l'entreprise.

Les exemples de données et toutes les définitions de vue sont incorporés dans ce SQL Fiddle afin qu'ils puissent être observés "en action".

Manipulation des données: code (s) du programme d'application et alias de colonne

L'utilisation du code des programmes d'application (si c'est ce que vous entendez par "code spécifique côté serveur") et des alias de colonne sont d'autres points importants que vous avez soulevés dans les commentaires suivants:

  • J'ai réussi à contourner un problème [JOIN] avec du code spécifique côté serveur, mais je ne veux vraiment pas que cela -et- l'ajout d'alias à toutes les colonnes soit "stressant".

  • Très bien expliqué, merci beaucoup. Cependant, comme je le soupçonnais, je devrai manipuler l'ensemble de résultats lors de la liste de toutes les données en raison des similitudes avec certaines colonnes, car je ne veux pas utiliser plusieurs alias pour garder la déclaration plus propre.

Il est opportun d'indiquer que l'utilisation du code de programme d'application est une ressource très appropriée pour gérer les fonctionnalités de présentation (ou graphiques) des jeux de résultats, éviter la récupération de données ligne par ligne est primordial pour éviter les problèmes de vitesse d'exécution. L'objectif devrait être de "récupérer" les ensembles de données pertinents au total à l'aide des instruments de manipulation de données robustes fournis par le moteur de définition (précis) de la plate-forme SQL afin que vous puissiez optimiser le comportement de votre système.

En outre, l'utilisation d'alias pour renommer une ou plusieurs colonnes dans une certaine portée peut sembler stressante mais, personnellement, je considère cette ressource comme un outil très puissant qui aide à (i) contextualiser et (ii) lever l'ambiguïté de la signification et intention attribuée aux colonnes concernées; par conséquent, c'est un aspect qui devrait être soigneusement réfléchi en ce qui concerne la manipulation des données d'intérêt.

Scénarios similaires

Vous pourriez aussi bien trouver de l'aide cette série de messages et ce groupe de messages qui contiennent mon point de vue sur deux autres cas qui incluent des associations de sous-types de sous-types avec des sous-types mutuellement exclusifs.

J'ai également proposé une solution pour un environnement commercial impliquant un cluster supertype-sous-type où les sous-types ne sont pas mutuellement exclusifs dans cette (plus récente) réponse .


Notes de fin

1   Définition d'intégration pour la modélisation de l'information ( IDEF1X ) est une technique de modélisation de données hautement recommandable qui a été établie en tant que norme en décembre 1993 par les États-Unis National Institute of Standards and Technology (NIST). Il est solidement basé sur (a) certains des travaux théoriques rédigés par le seul créateur du modèle relationnel , c'est-à-dire Dr EF Codd ; sur (b) la vue entité-relation , développée par Dr P. P. Chen ; et également sur (c) la technique de conception de bases de données logiques, créée par Robert G. Brown.

2  Dans IDEF1X, un nom de rôle est une étiquette distinctive affectée à une propriété (ou attribut) FK afin d'exprimer la signification qu'il détient dans le cadre de son type d'entité respectif.

3  La norme IDEF1X définit la migration de clé comme "le processus de modélisation consistant à placer la clé primaire d'un parent ou d'une entité générique dans son entité enfant ou catégorie comme clé étrangère".

18
MDCCL

Appelons la table principale Produits. Cela héberge les attributs partagés. Disons alors que nous avons une table Car, une table Plane et une table Boat. Ces trois tables auraient une clé ProductID avec une contrainte FK sur la ligne ID de la table Product. Si vous les voulez tous, rejoignez-les. Si vous ne voulez que les voitures, joignez à gauche Cars with Products (ou joignez à droite les produits et les voitures, mais je préfère toujours utiliser les jointures gauche).

C'est ce qu'on appelle un modèle de données hiearchical. Pour un petit nombre de sous-tableaux, cela peut avoir du sens dans un tableau long (millions de produits).

0
neManiac