web-dev-qa-db-fra.com

Possible de faire une clé étrangère MySQL à l'une des deux tables possibles?

Eh bien voici mon problème j'ai trois tables; régions, pays, états. Les pays peuvent être à l'intérieur de régions, les États peuvent être à l'intérieur de régions. Les régions sont le sommet de la chaîne alimentaire.

Maintenant, j'ajoute une table popular_areas avec deux colonnes; region_id et popular_place_id. Est-il possible de faire de popular_place_id une clé étrangère pour l'un des pays [~ # ~] ou [~ # ~] . Je vais probablement devoir ajouter une colonne popular_place_type pour déterminer si l'identifiant décrit un pays ou un état d'une manière ou d'une autre.

165
Andrew G. Johnson

Ce que vous décrivez s'appelle des associations polymorphes. En d'autres termes, la colonne "clé étrangère" contient une valeur id qui doit exister dans l'une des tables cible. En règle générale, les tables cible sont liées d’une certaine manière, par exemple en tant qu’instances d’une superclasse de données commune. Vous aurez également besoin d'une autre colonne à côté de la colonne de clé étrangère afin de pouvoir indiquer sur chaque ligne 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 des associations polymorphiques à l'aide de contraintes SQL. Une contrainte de clé étrangère fait toujours référence à une table cible.

Les associations polymorphiques sont supportées 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 garantir que référence est satisfaite, c’est-à-dire que la valeur de la clé étrangère est présente dans l’une des tables cibles possibles.

Les associations polymorphiques 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 de tous les clients accédant à la base de données avec la même logique d'intégrité référentielle mise en application, et l'application doit également être exempte de bogues.

Voici quelques solutions alternatives qui tirent parti de l'intégrité référentielle imposée par la base de données:

Créez une table supplémentaire par cible. Par exemple, popular_states et popular_countries, qui référencent 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 lieux préférés des utilisateurs, vous devez interroger ces deux tables. Mais cela signifie que vous pouvez compter sur la base de données pour appliquer la cohérence.

Créez une table places comme supertable. Comme le mentionne Abie, une deuxième alternative est que vos lieux populaires référencent une table comme places, qui est le parent de states et de countries. Autrement dit, les États et les pays ont également une clé étrangère vers places (vous pouvez même faire en sorte que cette clé étrangère soit également la clé primaire de 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 pouvant faire référence à l'une des deux tables cible, utilisez deux colonnes. Ces deux colonnes peuvent être NULL; en fait, un seul d'entre eux devrait ê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 polymorphiques violent la loi première forme normale , car le popular_place_id est en fait une colonne avec deux significations: c’est un état ou un pays. Vous ne stockeriez pas le age d'une personne et son phone_number dans une seule colonne et pour la même raison, vous ne devez pas stocker les deux state_id et country_id dans une seule colonne. Le fait que ces deux attributs possèdent des types de données compatibles est une coïncidence; ils signifient toujours différentes entités logiques.

Les associations polymorphiques violent également troisième forme normale , car la signification de la colonne dépend de la colonne supplémentaire nommant 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.


Commentaire de @SavasVedova:

Je ne suis pas sûr de suivre votre description sans consulter les définitions de table ou un exemple de requête, mais il semblerait que vous ayez simplement plusieurs tables Filters, chacune contenant une clé étrangère faisant 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...

Il est facile de joindre les produits à un type de filtre spécifique si vous savez à quel type de filtre vous souhaitez vous connecter:

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 requiert que la table soit spécifiée et corrigée au moment de l'écriture de la requête. Vous ne pouvez pas choisir la table jointe de manière dynamique en fonction des valeurs trouvées dans les lignes individuelles de Products.

La seule autre option consiste à joindre à tous les tables de filtrage à l'aide de jointures externes. Ceux qui n'ont pas product_id correspondant seront simplement renvoyés sous la forme d'une seule ligne de null. Mais vous devez toujours coder en dur tous les tables jointes, et si vous ajoutez de nouvelles tables de filtres, 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.

251
Bill Karwin

Ce n'est pas la solution la plus élégante au monde, mais vous pouvez utiliser héritage de table en béton pour que cela fonctionne.

Conceptuellement, vous proposez la notion de classe de "choses pouvant être des zones populaires" dont héritent vos trois types de lieux. Vous pouvez le représenter sous forme de table appelée, par exemple, places, où chaque ligne a une relation un à un avec une ligne dans regions, countries ou states. (Les attributs partagés entre des régions, des pays ou des États, le cas échéant, pourraient être insérés dans cette table des espaces.) Votre popular_place_id serait alors une référence de clé étrangère à une ligne de la table des emplacements qui vous mènerait ensuite à une région, un pays ou un état.

La solution que vous proposez avec une deuxième colonne pour décrire le type d’association est la suivante: Rails gère les associations polymorphes, mais je ne suis pas un fan de cette idée en général. Bill explique très en détail pourquoi les associations polymorphes ne sont pas vos amis.

10
Abie

Voici une correction à l'approche "supertable" de Bill Karwin, utilisant une clé composée ( place_type, place_id ) pour résoudre les violations de forme normales perçues:

CREATE TABLE places (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) NOT NULL
     CHECK ( place_type = 'state', 'country' ),
  UNIQUE ( place_type, place_id )
);

CREATE TABLE states (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) DEFAULT 'state' NOT NULL
     CHECK ( place_type = 'state' ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
  -- attributes specific to states go here
);

CREATE TABLE countries (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) DEFAULT 'country' NOT NULL
     CHECK ( place_type = 'country' ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
  -- attributes specific to country go here
);

CREATE TABLE popular_areas (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  UNIQUE ( user_id, place_id ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
);

Ce que cette conception ne peut pas garantir, il existe une ligne dans places ou states pour countries (mais pas les deux). Ceci est une limitation des clés étrangères en SQL. Dans un SGBD complet conforme aux normes SQL-92, vous pouvez définir des contraintes inter-tables pouvant être différées qui vous permettraient d'atteindre le même objectif, mais il est difficile à gérer, implique une transaction et un tel SGBD n'a pas encore été mis sur le marché.

5
onedaywhen

Je me rends compte que ce fil est vieux, mais je l'ai vu et une solution m'est venue à l'esprit et je pensais le jeter.

Les régions, les pays et les États sont des lieux géographiques qui vivent dans une hiérarchie.

Vous pourriez éviter complètement votre problème en créant une table de domaine appelée geo_location_type que vous renseigneriez avec trois lignes (Région, Pays, État).

Ensuite, au lieu des trois tables d'emplacement, créez une table unique géolocalisation qui possède une clé étrangère de type_local_traitement géographique (afin de savoir si l'instance est une région, un pays ou un État).

Modélisez la hiérarchie en faisant de cette table une référence automatique de sorte qu'une instance State conserve la clé fKey dans son instance de pays parent, laquelle à son tour contient la clé fKey dans son instance de région parent. Les instances de région contiendraient NULL dans cette clé. Ce n’est pas différent de ce que vous auriez fait avec les trois tables (vous auriez 1 - beaucoup de relations entre la région et le pays et entre le pays et l’état), sauf que tout est maintenant dans une table.

La table popular_user_location serait une table de résolution de portée entre user et georgraphical_location (de nombreux utilisateurs pourraient donc aimer beaucoup d'endroits).

Soooo…

enter image description here

CREATE TABLE [geographical_location_type] (
    [geographical_location_type_id] INTEGER NOT NULL,
    [name] VARCHAR(25) NOT NULL,
    CONSTRAINT [PK_geographical_location_type] PRIMARY KEY ([geographical_location_type_id])
)

-- Add 'Region', 'Country' and 'State' instances to the above table


CREATE TABLE [geographical_location] (
   [geographical_location_id] BIGINT IDENTITY(0,1) NOT NULL,
    [name] VARCHAR(1024) NOT NULL,
    [geographical_location_type_id] INTEGER NOT NULL,
    [geographical_location_parent] BIGINT,  -- self referencing; can be null for top-level instances
    CONSTRAINT [PK_geographical_location] PRIMARY KEY ([geographical_location_id])
)

CREATE TABLE [user] (
    [user_id] BIGINT NOT NULL,
    [login_id] VARCHAR(30) NOT NULL,
    [password] VARCHAR(512) NOT NULL,
    CONSTRAINT [PK_user] PRIMARY KEY ([user_id])
)


CREATE TABLE [popular_user_location] (
    [popular_user_location_id] BIGINT NOT NULL,
    [user_id] BIGINT NOT NULL,
    [geographical_location_id] BIGINT NOT NULL,
    CONSTRAINT [PK_popular_user_location] PRIMARY KEY ([popular_user_location_id])
)

ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_type_geographical_location] 
    FOREIGN KEY ([geographical_location_type_id]) REFERENCES [geographical_location_type] ([geographical_location_type_id])



ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_geographical_location] 
    FOREIGN KEY ([geographical_location_parent]) REFERENCES [geographical_location] ([geographical_location_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [user_popular_user_location] 
    FOREIGN KEY ([user_id]) REFERENCES [user] ([user_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [geographical_location_popular_user_location] 
    FOREIGN KEY ([geographical_location_id]) REFERENCES [geographical_location] ([geographical_location_id])

Je ne savais pas quelle était la base de données cible; ce qui précède est MS SQL Server.

0
Toolsmythe