Pourquoi ne pouvez-vous pas une clé étrangère dans une association polymorphique?
Question
Pourquoi ne pouvez-vous pas une clé étrangère dans une association polymorphique, comme celui représenté ci-dessous comme modèle Rails?
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
class Article < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Photo < ActiveRecord::Base
has_many :comments, :as => :commentable
#...
end
class Event < ActiveRecord::Base
has_many :comments, :as => :commentable
end
La solution
Une clé étrangère doit faire référence à une seule table parent. Ceci est fondamental pour la théorie de la syntaxe SQL et relationnelle.
Une association polymorphique est quand une colonne donnée peut faire référence à l'une des deux ou plusieurs tables parent. Il n'y a aucun moyen que vous pouvez déclarer cette contrainte dans SQL.
Les règles brise la conception des associations polymorphiques de conception de base de données relationnelle. Je ne recommande pas l'utiliser.
Il existe plusieurs alternatives:
-
Arcs Exclusif: Créer plusieurs colonnes de clé étrangère, chaque référence à l'un des parents. Faire respecter exactement l'une de ces clés étrangères peuvent être non NULL.
-
Inverser la relation:. Utilisez trois nombreux à plusieurs tables, chacune des références Commentaires et un parent respectif
-
Béton Supertable: Au lieu de l'implicite "commentable" superclasse, créer une vraie table que chacune de vos tables de références parents. Ensuite, liez vos commentaires à ce supertable. code de pseudo-rails serait quelque chose comme ce qui suit (je ne suis pas un utilisateur Rails, donc traiter cela comme une ligne directrice, pas de code littéral):
class Commentable < ActiveRecord::Base has_many :comments end class Comment < ActiveRecord::Base belongs_to :commentable end class Article < ActiveRecord::Base belongs_to :commentable end class Photo < ActiveRecord::Base belongs_to :commentable end class Event < ActiveRecord::Base belongs_to :commentable end
Je couvre aussi des associations polymorphiques dans ma présentation orienté objet pratique modèles SQL, et mon livre SQL Antipatterns: éviter les pièges de la programmation de base de données .
Re votre commentaire: Oui, je ne sais qu'il ya une autre colonne qui note le nom de la table que la clé étrangère fait pour soi-disant. Cette conception est pas pris en charge par des clés étrangères dans SQL.
Qu'est-ce qui se passe, par exemple, si vous insérez un commentaire et le nom « Vidéo » comme le nom de la table parent pour ce Comment
? Aucune table « vidéo » existe. l'insert doit être interrompu avec une erreur? Qu'est-ce que la contrainte est violé? Comment le SGBDR savent que cette colonne est censé nommer une table existante? Comment faut-il gérer les noms de table insensibles à la casse?
De même, si vous laissez tomber la table Events
, mais vous avez des lignes en Comments
qui indiquent des événements comme leurs parents, ce qui devrait être le résultat? la table de chute doit être annulée? Si les lignes de Comments
être devenus orphelins? En cas de changement de se référer à une autre table existante, comme Articles
? Est-ce que les valeurs id qui ont utilisé pour pointer vers Events
de sens en pointant à Articles
?
Ces dilemmes sont dus au fait que les associations polymorphes dépend de l'utilisation des données (à savoir une valeur de chaîne) pour se référer aux métadonnées (un nom de table). Ce ne sont pas pris en charge par SQL. Les données et les métadonnées sont séparées.
Je vais avoir du mal à envelopper ma tête autour de votre proposition « Concrete Supertable ».
-
Définir
Commentable
comme une vraie table SQL, pas seulement un adjectif dans votre définition du modèle Rails. Aucune autre colonnes sont nécessaires.CREATE TABLE Commentable ( id INT AUTO_INCREMENT PRIMARY KEY ) TYPE=InnoDB;
-
Définir les tables
Articles
,Photos
etEvents
comme "sous-classes" deCommentable
, en rendant leur clé primaire soit aussi unCommentable
de référencement clé étrangère.CREATE TABLE Articles ( id INT PRIMARY KEY, -- not auto-increment FOREIGN KEY (id) REFERENCES Commentable(id) ) TYPE=InnoDB; -- similar for Photos and Events.
-
Définir la table
Comments
avec une clé étrangère àCommentable
.CREATE TABLE Comments ( id INT PRIMARY KEY AUTO_INCREMENT, commentable_id INT NOT NULL, FOREIGN KEY (commentable_id) REFERENCES Commentable(id) ) TYPE=InnoDB;
-
Si vous voulez créer un
Article
(par exemple), vous devez créer une nouvelle ligne dansCommentable
aussi. Donc aussi pourPhotos
etEvents
.INSERT INTO Commentable (id) VALUES (DEFAULT); -- generate a new id 1 INSERT INTO Articles (id, ...) VALUES ( LAST_INSERT_ID(), ... ); INSERT INTO Commentable (id) VALUES (DEFAULT); -- generate a new id 2 INSERT INTO Photos (id, ...) VALUES ( LAST_INSERT_ID(), ... ); INSERT INTO Commentable (id) VALUES (DEFAULT); -- generate a new id 3 INSERT INTO Events (id, ...) VALUES ( LAST_INSERT_ID(), ... );
-
Si vous voulez créer un
Comment
, utilisez une valeur qui existe dansCommentable
.INSERT INTO Comments (id, commentable_id, ...) VALUES (DEFAULT, 2, ...);
-
Si vous voulez interroger les commentaires d'un
Photo
, faire un peu donné Jointures:SELECT * FROM Photos p JOIN Commentable t ON (p.id = t.id) LEFT OUTER JOIN Comments c ON (t.id = c.commentable_id) WHERE p.id = 2;
-
When vous suffit l'identifiant d'un commentaire et que vous voulez trouver ce que commentable ressources, il est un commentaire pour. Pour cela, vous trouverez peut-être qu'il est utile pour la table commentable pour désigner quelle ressource il fait référence.
SELECT commentable_id, commentable_type FROM Commentable t JOIN Comments c ON (t.id = c.commentable_id) WHERE c.id = 42;
Ensuite, vous aurez besoin d'exécuter une deuxième requête pour obtenir des données de la table des ressources respectives (Photos, articles, etc.), après avoir découvert de
commentable_type
la table à se joindre à. Vous ne pouvez pas le faire dans la même requête, car SQL nécessite que les tables soient nommés de manière explicite; vous ne pouvez pas joindre à une table déterminée par les résultats de données dans la même requête.
Il est vrai que certaines de ces étapes Casser les conventions utilisées par Rails. Mais les conventions Rails sont fausses en ce qui concerne la conception de base de données relationnelle appropriée.
Autres conseils
Bill Karwin est exact que les clés étrangères ne peuvent pas être utilisées avec des relations polymorphes en raison de SQL ne pas vraiment avoir un concept indigène relations polymorphes. Mais si votre objectif d'avoir une clé étrangère est de faire respecter l'intégrité référentielle, vous pouvez simuler via les déclencheurs. Cela devient spécifique DB, mais ci-dessous quelques éléments déclencheurs récents que j'ai créé pour simuler la suppression en cascade le comportement d'une clé étrangère sur une relation polymorphes:
CREATE FUNCTION delete_related_brokerage_subscribers() RETURNS trigger AS $$
BEGIN
DELETE FROM subscribers
WHERE referrer_type = 'Brokerage' AND referrer_id = OLD.id;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER cascade_brokerage_subscriber_delete
AFTER DELETE ON brokerages
FOR EACH ROW EXECUTE PROCEDURE delete_related_brokerage_subscribers();
CREATE FUNCTION delete_related_agent_subscribers() RETURNS trigger AS $$
BEGIN
DELETE FROM subscribers
WHERE referrer_type = 'Agent' AND referrer_id = OLD.id;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER cascade_agent_subscriber_delete
AFTER DELETE ON agents
FOR EACH ROW EXECUTE PROCEDURE delete_related_agent_subscribers();
Dans mon code d'un enregistrement de la table de brokerages
ou d'un enregistrement de la table de agents
peut se rapporter à un enregistrement de la table de subscribers
.