web-dev-qa-db-fra.com

Pourquoi ne pouvez-vous pas avoir de clé étrangère dans une association polymorphe?

Pourquoi ne pouvez-vous pas avoir de clé étrangère dans une association polymorphe, comme celle représentée ci-dessous sous la forme d'un 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
73
eggdrop

Une clé étrangère doit référencer une seule table parent. Ceci est fondamental à la fois pour la syntaxe SQL et la théorie relationnelle.

Une association polymorphe est lorsqu'une colonne donnée peut faire référence à deux ou plusieurs tables parentes. Il n'y a aucun moyen de déclarer cette contrainte en SQL.

La conception des associations polymorphes enfreint les règles de conception des bases de données relationnelles. Je ne recommande pas de l'utiliser.

Il existe plusieurs alternatives:

  • Arcs exclusifs: Créez plusieurs colonnes de clé étrangère, chacune référençant un parent. Faites en sorte qu'exactement l'une de ces clés étrangères puisse être non NULL.

  • Inverser la relation: Utilisez trois tables plusieurs-à-plusieurs, chacune faisant référence à des commentaires et à un parent respectif.

  • Supertable concret: Au lieu de la superclasse "commentable" implicite, créez une vraie table à laquelle chacune de vos tables parent fait référence. Liez ensuite vos commentaires à ce supertable. Le code pseudo-Rails serait quelque chose comme ceci (je ne suis pas un utilisateur Rails, alors traitez-le comme une ligne directrice, pas un 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 également les associations polymorphes dans ma présentation Modèles pratiques orientés objet en SQL , et mon livre SQL Antipatterns: éviter les pièges de la programmation de base de données .


Re votre commentaire: Oui, je sais qu'il y a une autre colonne qui note le nom de la table vers laquelle la clé étrangère pointe. Cette conception n'est pas prise en charge par les clés étrangères dans SQL.

Que se passe-t-il, par exemple, si vous insérez un commentaire et nommez "Video" comme nom de la table parent pour ce Comment? Aucune table nommée "Vidéo" n'existe. L'insert doit-il être abandonné avec une erreur? Quelle contrainte est violée? Comment le SGBDR sait-il que cette colonne est censée nommer une table existante? Comment gère-t-il les noms de table insensibles à la casse?

De même, si vous supprimez la table Events, mais que vous avez des lignes dans Comments qui indiquent des événements comme parent, quel devrait être le résultat? La table basse doit-elle être abandonnée? Les lignes de Comments doivent-elles être orphelines? Doivent-ils changer pour faire référence à une autre table existante telle que Articles? Les valeurs d'ID qui pointaient vers Events ont-elles un sens lorsque vous pointez vers Articles?

Ces dilemmes sont tous dus au fait que les associations polymorphes dépendent de l'utilisation de données (c'est-à-dire une valeur de chaîne) pour faire référence aux métadonnées (un nom de table). Ceci n'est pas pris en charge par SQL. Les données et les métadonnées sont distinctes.


J'ai du mal à m'enrouler autour de votre proposition "Concrete Supertable".

  • Définissez Commentable comme une vraie table SQL, pas seulement un adjectif dans votre définition de modèle Rails. Aucune autre colonne n'est nécessaire.

    CREATE TABLE Commentable (
      id INT AUTO_INCREMENT PRIMARY KEY
    ) TYPE=InnoDB;
    
  • Définissez les tables Articles, Photos et Events comme des "sous-classes" de Commentable, en faisant de leur clé primaire une clé étrangère référençant Commentable.

    CREATE TABLE Articles (
      id INT PRIMARY KEY, -- not auto-increment
      FOREIGN KEY (id) REFERENCES Commentable(id)
    ) TYPE=InnoDB;
    
    -- similar for Photos and Events.
    
  • Définissez la table Comments avec une clé étrangère sur Commentable.

    CREATE TABLE Comments (
      id INT PRIMARY KEY AUTO_INCREMENT,
      commentable_id INT NOT NULL,
      FOREIGN KEY (commentable_id) REFERENCES Commentable(id)
    ) TYPE=InnoDB;
    
  • Lorsque vous souhaitez créer un Article (par exemple), vous devez également créer une nouvelle ligne dans Commentable. De même pour Photos et Events.

    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(), ... );
    
  • Lorsque vous souhaitez créer un Comment, utilisez une valeur qui existe dans Commentable.

    INSERT INTO Comments (id, commentable_id, ...)
    VALUES (DEFAULT, 2, ...);
    
  • Lorsque vous souhaitez interroger les commentaires d'un Photo donné, faites quelques 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;
    
  • Lorsque vous n'avez que l'ID d'un commentaire et que vous souhaitez trouver pour quelle ressource commentable il s'agit d'un commentaire. Pour cela, vous trouverez peut-être utile que le tableau Commentable désigne la ressource à laquelle 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 devez exécuter une deuxième requête pour obtenir les données de la table de ressources respective (photos, articles, etc.), après avoir découvert à partir de commentable_type à quelle table se joindre. Vous ne pouvez pas le faire dans la même requête, car SQL requiert que les tables soient nommées explicitement; vous ne pouvez pas vous joindre à une table déterminée par les résultats des données dans la même requête.

Certes, certaines de ces étapes violent les conventions utilisées par Rails. Mais les conventions Rails sont incorrectes en ce qui concerne la conception appropriée de la base de données relationnelle.

165
Bill Karwin

Bill Karwin a raison de dire que les clés étrangères ne peuvent pas être utilisées avec des relations polymorphes car SQL n'a pas vraiment de concept polymorphe natif. Mais si votre objectif d'avoir une clé étrangère est d'imposer l'intégrité référentielle, vous pouvez la simuler via des déclencheurs. Cela devient spécifique à la base de données, mais voici quelques déclencheurs récents que j'ai créés pour simuler le comportement de suppression en cascade d'une clé étrangère sur une relation polymorphe:

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, un enregistrement de la table brokerages ou un enregistrement de la table agents peut être lié à un enregistrement de la table subscribers.

0
Eric Anderson