Je réfléchis à la façon de représenter une structure complexe dans une base de données SQL Server.
Prenons une application qui doit stocker les détails d’une famille d’objets, qui partagent certains attributs mais en ont d’autres peu communs. Par exemple, un package d'assurance commerciale peut inclure une couverture responsabilité, moteur, biens et indemnités dans le même dossier de police.
Il est facile d'implémenter cela en C #, etc., car vous pouvez créer une stratégie avec une collection de sections, où Section est héritée comme requis pour les différents types de couverture. Cependant, les bases de données relationnelles ne semblent pas le permettre facilement.
Je peux voir qu'il y a deux choix principaux:
Créez une table de stratégie, puis une table de sections, avec tous les champs requis, pour toutes les variantes possibles, dont la plupart seraient nulles.
Créez une table de règles et de nombreuses tables de sections, une pour chaque type de couverture.
Ces deux alternatives ne semblent pas satisfaisantes, d'autant plus qu'il est nécessaire d'écrire des requêtes dans toutes les sections, ce qui impliquerait de nombreuses jointures ou de nombreuses vérifications nulles.
Quelle est la meilleure pratique pour ce scénario?
@ Bill Karwin décrit trois modèles d'héritage dans son livre SQL Antipatterns , lorsqu'il propose des solutions au SQL Entity-Attribute-Value antipattern. Ceci est un bref aperçu:
Utiliser une seule table comme dans votre première option est probablement la conception la plus simple. Comme vous l'avez mentionné, beaucoup d'attributs spécifiques à un sous-type devront se voir attribuer une valeur NULL
sur les lignes où ces attributs ne s'appliquent pas. Avec ce modèle, vous auriez une seule table de règles, qui ressemblerait à ceci:
+------+---------------------+----------+----------------+------------------+
| id | date_issued | type | vehicle_reg_no | property_address |
+------+---------------------+----------+----------------+------------------+
| 1 | 2010-08-20 12:00:00 | MOTOR | 01-A-04004 | NULL |
| 2 | 2010-08-20 13:00:00 | MOTOR | 02-B-01010 | NULL |
| 3 | 2010-08-20 14:00:00 | PROPERTY | NULL | Oxford Street |
| 4 | 2010-08-20 15:00:00 | MOTOR | 03-C-02020 | NULL |
+------+---------------------+----------+----------------+------------------+
\------ COMMON FIELDS -------/ \----- SUBTYPE SPECIFIC FIELDS -----/
Garder la conception simple est un avantage, mais les principaux problèmes de cette approche sont les suivants:
Lorsqu'il s'agit d'ajouter de nouveaux sous-types, vous devez modifier le tableau pour prendre en compte les attributs décrivant ces nouveaux objets. Cela peut rapidement devenir problématique lorsque vous avez plusieurs sous-types ou si vous prévoyez d’ajouter des sous-types régulièrement.
La base de données ne pourra pas imposer quels attributs s'appliquent ou non, car aucune métadonnée ne permet de définir quels attributs appartiennent à quels sous-types.
Vous ne pouvez pas non plus appliquer NOT NULL
sur les attributs d'un sous-type qui devrait être obligatoire. Vous devrez gérer cela dans votre application, ce qui en général n'est pas idéal.
Une autre approche pour aborder l'héritage consiste à créer une nouvelle table pour chaque sous-type, en répétant tous les attributs communs de chaque table. Par exemple:
--// Table: policies_motor
+------+---------------------+----------------+
| id | date_issued | vehicle_reg_no |
+------+---------------------+----------------+
| 1 | 2010-08-20 12:00:00 | 01-A-04004 |
| 2 | 2010-08-20 13:00:00 | 02-B-01010 |
| 3 | 2010-08-20 15:00:00 | 03-C-02020 |
+------+---------------------+----------------+
--// Table: policies_property
+------+---------------------+------------------+
| id | date_issued | property_address |
+------+---------------------+------------------+
| 1 | 2010-08-20 14:00:00 | Oxford Street |
+------+---------------------+------------------+
Cette conception résoudra les problèmes identifiés pour la méthode de table unique:
Les attributs obligatoires peuvent maintenant être appliqués avec NOT NULL
.
L'ajout d'un nouveau sous-type nécessite l'ajout d'une nouvelle table au lieu d'ajouter des colonnes à une table existante.
Il n'y a également aucun risque qu'un attribut inapproprié soit défini pour un sous-type particulier, tel que le vehicle_reg_no
champ pour une politique de propriété.
L'attribut type
n'est pas nécessaire comme dans la méthode à table unique. Le type est maintenant défini par les métadonnées: le nom de la table.
Cependant, ce modèle a aussi quelques inconvénients:
Les attributs communs sont mélangés aux attributs spécifiques au sous-type et il n’existe pas de moyen facile de les identifier. La base de données ne le saura pas non plus.
Lors de la définition des tables, vous devrez répéter les attributs communs pour chaque table de sous-types. Ce n'est certainement pas SEC .
La recherche de toutes les stratégies, quel que soit le sous-type, devient difficile et nécessite un groupe de UNION
s.
Voici comment vous devez interroger toutes les stratégies, quel que soit leur type:
SELECT date_issued, other_common_fields, 'MOTOR' AS type
FROM policies_motor
UNION ALL
SELECT date_issued, other_common_fields, 'PROPERTY' AS type
FROM policies_property;
Notez que l'ajout de nouveaux sous-types nécessiterait la modification de la requête ci-dessus avec un UNION ALL
pour chaque sous-type. Cela peut facilement entraîner des bugs dans votre application si cette opération est oubliée.
C'est la solution que @ David mentionne dans l'autre réponse . Vous créez une seule table pour votre classe de base, qui inclut tous les attributs communs. Ensuite, vous créez des tables spécifiques pour chaque sous-type, dont la clé primaire sert également de clé étrangère à la table de base. Exemple:
CREATE TABLE policies (
policy_id int,
date_issued datetime,
-- // other common attributes ...
);
CREATE TABLE policy_motor (
policy_id int,
vehicle_reg_no varchar(20),
-- // other attributes specific to motor insurance ...
FOREIGN KEY (policy_id) REFERENCES policies (policy_id)
);
CREATE TABLE policy_property (
policy_id int,
property_address varchar(20),
-- // other attributes specific to property insurance ...
FOREIGN KEY (policy_id) REFERENCES policies (policy_id)
);
Cette solution résout les problèmes identifiés dans les deux autres conceptions:
Les attributs obligatoires peuvent être appliqués avec NOT NULL
.
L'ajout d'un nouveau sous-type nécessite l'ajout d'une nouvelle table au lieu d'ajouter des colonnes à une table existante.
Aucun risque qu'un attribut inapproprié ne soit défini pour un sous-type particulier.
Pas besoin de l'attribut type
.
Maintenant, les attributs communs ne sont plus mélangés avec les attributs spécifiques au sous-type.
Nous pouvons rester au sec, enfin. Il n'est pas nécessaire de répéter les attributs communs pour chaque table de sous-type lors de la création des tables.
La gestion d'une incrémentation automatique id
pour les stratégies devient plus facile car elle peut être gérée par la table de base au lieu de chaque table de sous-type les générant indépendamment.
La recherche de toutes les stratégies, quel que soit le sous-type, devient désormais très facile: pas besoin de UNION
s - juste un SELECT * FROM policies
.
Je considère l'approche de la table de classe comme la plus appropriée dans la plupart des situations.
Les noms de ces trois modèles proviennent de Martin Fowler book Patterns of Enterprise Application Architecture .
La troisième option consiste à créer une table "Stratégie", puis une table "SectionsMain" qui stocke tous les champs communs entre les types de sections. Créez ensuite d'autres tables pour chaque type de section contenant uniquement les champs qui ne sont pas en commun.
Le choix du meilleur dépend principalement du nombre de champs que vous avez et de la manière dont vous voulez écrire votre code SQL. Ils travailleraient tous. Si vous avez seulement quelques champs, je choisirais probablement le n ° 1. Avec "beaucoup" de champs je me pencherais vers # 2 ou # 3.
Avec les informations fournies, je modéliserais la base de données comme suit:
... et ainsi de suite, car je m'attendrais à ce que différents attributs soient associés à chaque section de la stratégie. Sinon, il pourrait y avoir une seule table SECTIONS
et en plus de la table policy_id
, il y aurait un section_type_code
...
Dans les deux cas, cela vous permettrait de prendre en charge des sections facultatives par stratégie ...
Je ne comprends pas ce que vous trouvez insatisfaisant à propos de cette approche - c’est ainsi que vous stockez des données tout en maintenant l’intégrité référentielle, sans les dupliquer. Le terme est "normalisé" ...
Comme SQL est basé sur SET, il est plutôt étranger aux concepts de programmation procédurale/OO et requiert du code pour passer d'un royaume à l'autre. Les ORM sont souvent pris en compte, mais ils ne fonctionnent pas bien dans les systèmes complexes à volume élevé.
Une autre façon de le faire consiste à utiliser le composant INHERITS
. Par exemple:
CREATE TABLE person (
id int ,
name varchar(20),
CONSTRAINT pessoa_pkey PRIMARY KEY (id)
);
CREATE TABLE natural_person (
social_security_number varchar(11),
CONSTRAINT pessoaf_pkey PRIMARY KEY (id)
) INHERITS (person);
CREATE TABLE juridical_person (
tin_number varchar(14),
CONSTRAINT pessoaj_pkey PRIMARY KEY (id)
) INHERITS (person);
Il est donc possible de définir un héritage entre les tables.
En outre, dans la solution Daniel Vassallo, si vous utilisez SQL Server 2016, il existe une autre solution que j'ai utilisée dans certains cas sans perte de performances considérable.
Vous pouvez créer uniquement une table avec uniquement le champ commun et ajouter une seule colonne avec la chaîne JSON qui contient tous les champs spécifiques au sous-type.
J'ai testé cette conception pour gérer l'héritage et je suis très heureux de la flexibilité que je peux utiliser dans l'application relative.
Je me penche vers la méthode n ° 1 (une table de sections unifiée), dans le but de récupérer efficacement des stratégies entières avec toutes leurs sections (ce qui, je suppose, sera grandement optimisé par votre système).
De plus, je ne sais pas quelle version de SQL Server vous utilisez, mais en 2008+ Colonnes creuses aide à optimiser les performances dans les situations où beaucoup des valeurs d'une colonne seront NULL.
En fin de compte, vous devrez décider à quel point les sections de politique sont "similaires". À moins d'une différence substantielle, je pense qu'une solution plus normalisée pourrait poser plus de problèmes qu'elle n'en vaut la peine ... mais vous seul pouvez passer cet appel. :)