J'ai une table containers
qui peut avoir une relation plusieurs à plusieurs avec plusieurs tables, disons que ce sont plants
, animals
et bacteria
. Chaque conteneur peut contenir un nombre arbitraire de plantes, d'animaux ou de bactéries, et chaque plante, animal ou bactérie peut se trouver dans un nombre arbitraire de conteneurs.
Jusqu'à présent, c'est très simple, mais la partie avec laquelle je rencontre un problème est que chaque conteneur ne doit contenir que des éléments du même type. Les conteneurs mixtes contenant par exemple à la fois des plantes et des animaux devraient constituer une violation des contraintes dans la base de données.
Mon schéma d'origine pour cela était le suivant:
containers
----------
id
...
...
containers_plants
-----------------
container_id
plant_id
containers_animals
------------------
container_id
animal_id
containers_bacteria
-------------------
container_id
bacterium_id
Mais avec ce schéma, je ne peux pas trouver comment implémenter la contrainte que les conteneurs doivent être homogènes.
Existe-t-il un moyen de l'implémenter avec une intégrité référentielle et de garantir au niveau de la base de données que les conteneurs sont homogènes?
J'utilise Postgres 9.6 pour cela.
la source
Réponses:
Il existe un moyen de l'implémenter de manière déclarative uniquement sans modifier considérablement votre configuration actuelle, si vous acceptez d'y introduire une certaine redondance. Ce qui suit peut être considéré comme un développement de la suggestion de RDFozz , bien que l'idée se soit pleinement formée dans mon esprit avant de lire sa réponse (et elle est suffisamment différente pour justifier sa propre réponse de toute façon).
la mise en oeuvre
Voici ce que vous faites, étape par étape:
Créez un
containerTypes
tableau dans le sens de celui suggéré dans la réponse de RDFozz:Remplissez-le avec des ID prédéfinis pour chaque type. Aux fins de cette réponse, laissez-les correspondre à l'exemple de RDFozz: 1 pour les plantes, 2 pour les animaux, 3 pour les bactéries.
Ajoutez une
containerType_id
colonne àcontainers
et rendez-la non nullable et une clé étrangère.En supposant que la
id
colonne est déjà la clé primaire decontainers
, créez une contrainte unique sur(id, containerType_id)
.C'est là que les licenciements commencent. Si
id
est déclaré être la clé primaire, nous pouvons être assurés qu'il est unique. S'il est unique, toute combinaison deid
et une autre colonne est aussi nécessairement unique sans déclaration supplémentaire d'unicité - alors, quel est le point? Le fait est qu'en déclarant formellement la paire de colonnes unique, nous les laissons être référencables , c'est-à-dire être la cible d'une contrainte de clé étrangère, ce qui est le sujet de cette partie.Ajouter une
containerType_id
colonne à chacune des tables de jonction (containers_animals
,containers_plants
,containers_bacteria
). En faire une clé étrangère est complètement facultatif. Ce qui est crucial est de s'assurer que la colonne a la même valeur pour toutes les lignes, différente pour chaque table: 1 pourcontainers_plants
, 2 pourcontainers_animals
, 3 pourcontainers_bacteria
, selon les descriptions decontainerTypes
. Dans chaque cas, vous pouvez également définir cette valeur par défaut pour simplifier vos instructions d'insertion:Dans chacune des tables de jonction, faites de la paire de colonnes
(container_id, containerType_id)
une référence de contrainte de clé étrangèrecontainers
.Si
container_id
est déjà défini comme référencecontainers
, n'hésitez pas à supprimer cette contrainte de chaque table car elle n'est plus nécessaire.Comment ça fonctionne
En ajoutant la colonne de type de conteneur et en la faisant participer aux contraintes de clé étrangère, vous préparez un mécanisme empêchant le type de conteneur de changer. Changer le type dans le
containers
type ne serait possible que si les clés étrangères étaient définies avec laDEFERRABLE
clause, qu'elles ne sont pas censées être dans cette implémentation.Même s'ils pouvaient être reportés, le changement de type serait toujours impossible en raison de la contrainte de vérification de l'autre côté de la
containers
relation entre la table de jonction. Chaque table de jonction autorise un seul type de conteneur spécifique. Cela empêche non seulement les références existantes de modifier le type, mais empêche également l' ajout de références de type incorrectes. Autrement dit, si vous avez un conteneur de type 2 (animaux), vous ne pouvez y ajouter des éléments qu'en utilisant le tableau où le type 2 est autorisé, ce qui estcontainers_animals
, et ne serait pas en mesure d'ajouter des lignes le référençant, disonscontainers_bacteria
, qui accepte uniquement des conteneurs de type 3.Enfin, votre décision d'avoir des tables différentes pour
plants
,animals
etbacteria
, et différentes tables de jonction pour chaque type d'entité, rend déjà impossible pour un conteneur d'avoir des éléments de plus d'un type.Ainsi, tous ces facteurs combinés garantissent, de manière purement déclarative, que tous vos conteneurs seront homogènes.
la source
Une option consiste à ajouter un
containertype_id
à laContainer
table. Faites de la colonne NOT NULL, et une clé étrangère vers uneContainerType
table, qui aurait des entrées pour chaque type d'élément pouvant aller dans un conteneur:Pour vous assurer que le type de conteneur ne peut pas être modifié, créez un déclencheur de mise à jour qui vérifie si le a
containertype_id
été mis à jour et annule la modification dans ce cas.Ensuite, dans les déclencheurs d'insertion et de mise à jour de vos tables de liens de conteneur, vérifiez le containerertype_id par rapport au type d'entité dans cette table, pour vous assurer qu'ils correspondent.
Si tout ce que vous mettez dans un conteneur doit correspondre au type et que le type ne peut pas être modifié, alors tout dans le conteneur sera du même type.
REMARQUE: Étant donné que le déclencheur sur les tables de liens détermine ce qui correspond, si vous aviez besoin d'un type de conteneur pouvant contenir des plantes et des animaux, vous pouvez créer ce type, l'affecter au conteneur et vérifier cela. . Ainsi, vous conservez la flexibilité si les choses changent à un moment donné (par exemple, vous obtenez les types "magazines" et "livres" ...).
REMARQUE la seconde: si la plupart de ce qui arrive aux conteneurs est le même, indépendamment de ce qu'ils contiennent, cela a du sens. Si vous avez des choses très différentes qui se produisent (dans le système, pas dans notre réalité physique) en fonction du contenu du conteneur, alors l'idée d'Evan Carroll d'avoir des tables séparées pour les types de conteneurs distincts est parfaitement logique. Cette solution établit que les conteneurs ont différents types lors de la création, mais les conserve dans la même table. Si vous devez vérifier le type à chaque fois que vous effectuez une action sur un conteneur, et si l'action que vous effectuez dépend du type, des tables séparées peuvent en fait être plus rapides et plus faciles.
la source
Si vous n'avez besoin que de 2 ou 3 catégories (plantes / métazoaires / bactéries) et que vous souhaitez modéliser une relation XOR, peut-être qu'un "arc" est la solution pour vous. Avantage: pas besoin de déclencheurs. Des exemples de diagrammes peuvent être trouvés [ici] [1]. Dans votre situation, la table "containers" aurait 3 colonnes avec une contrainte CHECK, permettant soit une plante, soit un animal, soit une bactérie.
Cela n'est probablement pas approprié s'il sera nécessaire de faire la distinction entre de nombreuses catégories (par exemple genres, espèces, sous-espèces) à l'avenir. Cependant, pour 2-3 groupes / catégories, cela peut faire l'affaire.
MISE À JOUR: Inspirée par les suggestions et commentaires du contributeur, une solution différente qui permet de nombreux taxons (groupes d'organismes apparentés, classés par biologiste), et évite les noms de table "spécifiques" (PostgreSQL 9.5).
Code DDL:
Données de test:
Essai:
Merci à @RDFozz et @Evan Carroll et @ypercube pour leur contribution et leur patience (lecture / correction de mes réponses).
la source
Tout d'abord, je suis d'accord avec @RDFozz sur la lecture de la question .. Cependant, il soulève quelques inquiétudes sur la réponse de Stefan ,
Pour répondre à ses préoccupations,
PRIMARY KEY
UNIQUE
contraintes pour vous protéger contre les entrées en double.EXCLUSION
contraintes pour garantir que les conteneurs sont "homogènes"c_id
pour garantir des performances décentes.Voici à quoi cela ressemble,
Maintenant, vous pouvez avoir un conteneur avec plusieurs choses, mais un seul type de chose dans un conteneur.
Et tout est implémenté sur les index GIST.
La Grande Pyramide de Gizeh n'a rien sur PostgreSQL.
la source
C'est une mauvaise idée.
Et maintenant tu sais pourquoi. =)
Je crois que vous êtes coincé sur l'idée de l'héritage de la programmation orientée objet (OO). OO Inheritance résout un problème de réutilisation de code. En SQL, le code redondant est le moindre de nos problèmes. L'intégrité est d'abord et avant tout. La performance est souvent deuxième. Nous savourerons de douleur pour les deux premiers. Nous n'avons pas de «temps de compilation» qui puisse éliminer les coûts.
Donc, renoncez à votre obsession pour la réutilisation du code. Les conteneurs pour plantes, animaux et bactéries sont fondamentalement différents partout dans le monde réel. Le composant de réutilisation de code de "détient des trucs" ne le fera tout simplement pas pour vous. Brisez-les. Non seulement vous obtiendrez plus d'intégrité et plus de performances, mais à l'avenir, vous trouverez plus facile d'étendre votre schéma: après tout, dans votre schéma, vous deviez déjà séparer les éléments contenus (plantes, animaux, etc.) , semble au moins possible que vous deviez briser les conteneurs. Vous n'allez pas vouloir repenser tout votre schéma alors.
la source
plant_containers
, etc. Les choses qui n'ont besoin que d'un conteneur de plantes ne sont sélectionnées que dans leplant_containers
tableau. Les choses qui ont besoin de n'importe quel conteneur (c'est-à-dire la recherche de tous les types de conteneurs) peuvent faireUNION ALL
sur les trois tables avec des conteneurs.