Ce que vous décrivez s'appelle des associations polymorphes. C'est-à-dire que la colonne "clé étrangère" contient une valeur id qui doit exister dans l'une des tables cibles d'un ensemble. En règle générale, les tables cibles sont liées d'une manière ou d'une autre, par exemple en étant des instances d'une superclasse commune de données. Vous aurez également besoin d'une autre colonne à côté de la colonne de clé étrangère, de sorte que sur chaque ligne, vous puissiez désigner la table cible référencée.
CREATE TABLE popular_places (
user_id INT NOT NULL,
place_id INT NOT NULL,
place_type VARCHAR(10) -- either 'states' or 'countries'
-- foreign key is not possible
);
Il n'y a aucun moyen de modéliser les associations polymorphes à l'aide de contraintes SQL. Une contrainte de clé étrangère fait toujours référence à une tableau cible.
Les associations polymorphes sont prises en charge par des frameworks tels que Rails et Hibernate. Mais ils disent explicitement que vous devez désactiver les contraintes SQL pour utiliser cette fonctionnalité. Au lieu de cela, l'application ou le framework doit effectuer un travail équivalent pour s'assurer que la référence est satisfaite. Autrement dit, la valeur de la clé étrangère est présente dans l'une des tables cibles possibles.
Les associations polymorphes sont faibles en ce qui concerne l'application de la cohérence de la base de données. L'intégrité des données dépend du fait que tous les clients accèdent à la base de données avec la même logique d'intégrité référentielle appliquée, et l'application doit également être sans bogue.
Voici quelques solutions alternatives qui tirent parti de l'intégrité référentielle renforcée par la base de données :
Créez un tableau supplémentaire par cible. Par exemple popular_states
et popular_countries
, qui référence states
et countries
respectivement. Chacune de ces tables "populaires" fait également référence au profil de l'utilisateur.
CREATE TABLE popular_states (
state_id INT NOT NULL,
user_id INT NOT NULL,
PRIMARY KEY(state_id, user_id),
FOREIGN KEY (state_id) REFERENCES states(state_id),
FOREIGN KEY (user_id) REFERENCES users(user_id),
);
CREATE TABLE popular_countries (
country_id INT NOT NULL,
user_id INT NOT NULL,
PRIMARY KEY(country_id, user_id),
FOREIGN KEY (country_id) REFERENCES countries(country_id),
FOREIGN KEY (user_id) REFERENCES users(user_id),
);
Cela signifie que pour obtenir tous les endroits préférés d'un utilisateur, vous devez interroger ces deux tables. Mais cela signifie que vous pouvez compter sur la base de données pour assurer la cohérence.
Créer un places
table comme supertable. Comme Abie le mentionne, une deuxième alternative est que vos lieux populaires référencent un tableau comme places
, qui est un parent des deux states
et countries
. Autrement dit, les états et les pays ont également une clé étrangère pour les places
(vous pouvez même faire en sorte que cette clé étrangère soit également la clé primaire des states
et countries
).
CREATE TABLE popular_areas (
user_id INT NOT NULL,
place_id INT NOT NULL,
PRIMARY KEY (user_id, place_id),
FOREIGN KEY (place_id) REFERENCES places(place_id)
);
CREATE TABLE states (
state_id INT NOT NULL PRIMARY KEY,
FOREIGN KEY (state_id) REFERENCES places(place_id)
);
CREATE TABLE countries (
country_id INT NOT NULL PRIMARY KEY,
FOREIGN KEY (country_id) REFERENCES places(place_id)
);
Utilisez deux colonnes. Au lieu d'une colonne qui peut faire référence à l'une des deux tables cibles, utilisez deux colonnes. Ces deux colonnes peuvent être NULL
; en fait, un seul d'entre eux doit être non NULL
.
CREATE TABLE popular_areas (
place_id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
state_id INT,
country_id INT,
CONSTRAINT UNIQUE (user_id, state_id, country_id), -- UNIQUE permits NULLs
CONSTRAINT CHECK (state_id IS NOT NULL OR country_id IS NOT NULL),
FOREIGN KEY (state_id) REFERENCES places(place_id),
FOREIGN KEY (country_id) REFERENCES places(place_id)
);
En termes de théorie relationnelle, les associations polymorphes violent la la première forme normale
, car le popular_place_id
est en effet une colonne à deux sens :c'est soit un état, soit un pays. Vous ne stockeriez pas l'age
d'une personne et leur phone_number
dans une seule colonne, et pour la même raison, vous ne devriez pas stocker les deux state_id
et country_id
dans une seule colonne. Le fait que ces deux attributs aient des types de données compatibles est une coïncidence; ils signifient toujours différentes entités logiques.
Les associations polymorphes violent également la troisième forme normale , car la signification de la colonne dépend de la colonne supplémentaire qui nomme la table à laquelle la clé étrangère fait référence. Dans la troisième forme normale, un attribut dans une table doit dépendre uniquement de la clé primaire de cette table.
Re commentaire de @SavasVedova :
Je ne suis pas sûr de suivre votre description sans voir les définitions de table ou un exemple de requête, mais il semble que vous ayez simplement plusieurs Filters
tables, chacune contenant une clé étrangère qui référence un Products
central table.
CREATE TABLE Products (
product_id INT PRIMARY KEY
);
CREATE TABLE FiltersType1 (
filter_id INT PRIMARY KEY,
product_id INT NOT NULL,
FOREIGN KEY (product_id) REFERENCES Products(product_id)
);
CREATE TABLE FiltersType2 (
filter_id INT PRIMARY KEY,
product_id INT NOT NULL,
FOREIGN KEY (product_id) REFERENCES Products(product_id)
);
...and other filter tables...
Joindre les produits à un type de filtre spécifique est facile si vous savez à quel type vous souhaitez vous joindre :
SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)
Si vous souhaitez que le type de filtre soit dynamique, vous devez écrire du code d'application pour construire la requête SQL. SQL exige que la table soit spécifiée et fixée au moment où vous écrivez la requête. Vous ne pouvez pas faire en sorte que le tableau joint soit choisi dynamiquement en fonction des valeurs trouvées dans les lignes individuelles de Products
.
La seule autre option est de rejoindre tous filtrer les tables à l'aide de jointures externes. Ceux qui n'ont pas de product_id correspondant seront simplement renvoyés sous la forme d'une seule ligne de valeurs nulles. Mais vous devez toujours coder en dur tous les tables jointes, et si vous ajoutez de nouvelles tables de filtrage, vous devez mettre à jour votre code.
SELECT * FROM Products
LEFT OUTER JOIN FiltersType1 USING (product_id)
LEFT OUTER JOIN FiltersType2 USING (product_id)
LEFT OUTER JOIN FiltersType3 USING (product_id)
...
Une autre façon de joindre toutes les tables de filtrage consiste à le faire en série :
SELECT * FROM Product
INNER JOIN FiltersType1 USING (product_id)
UNION ALL
SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)
UNION ALL
SELECT * FROM Products
INNER JOIN FiltersType3 USING (product_id)
...
Mais ce format nécessite toujours que vous écriviez des références à toutes les tables. Il n'y a pas moyen de contourner cela.