Warum können Sie keinen Fremdschlüssel in einer polymorphen Assoziation haben?

80

Warum können Sie in einer polymorphen Zuordnung keinen Fremdschlüssel haben, wie den unten als Rails-Modell dargestellten?

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
Eiertropfen
quelle
3
Nur aus Gründen der Klarheit anderer spricht das OP nicht über die foreign_keyOption, an die weitergegeben werden kann belongs_to. Das OP spricht von einer "Fremdschlüsseleinschränkung" der nativen Datenbank. Das hat mich eine Weile verwirrt.
Joshua Pinter

Antworten:

178

Ein Fremdschlüssel darf nur auf eine übergeordnete Tabelle verweisen. Dies ist sowohl für die SQL-Syntax als auch für die relationale Theorie von grundlegender Bedeutung.

Eine polymorphe Zuordnung liegt vor, wenn eine bestimmte Spalte auf eine oder zwei übergeordnete Tabellen verweisen kann. Es gibt keine Möglichkeit, diese Einschränkung in SQL zu deklarieren.

Das Design für polymorphe Assoziationen verstößt gegen die Regeln für das Design relationaler Datenbanken. Ich empfehle es nicht.

Es gibt verschiedene Alternativen:

  • Exklusive Bögen: Erstellen Sie mehrere Fremdschlüsselspalten, die jeweils auf ein übergeordnetes Element verweisen. Erzwingen Sie, dass genau einer dieser Fremdschlüssel nicht NULL sein kann.

  • Beziehung umkehren: Verwenden Sie drei Viele-zu-Viele-Tabellen, die jeweils auf Kommentare und ein übergeordnetes Element verweisen.

  • Konkrete Supertabelle: Erstellen Sie anstelle der impliziten "kommentierbaren" Oberklasse eine echte Tabelle, auf die jede Ihrer übergeordneten Tabellen verweist. Verknüpfen Sie dann Ihre Kommentare mit dieser Supertabelle. Pseudo-Rails-Code wäre ungefähr so ​​(ich bin kein Rails-Benutzer, behandeln Sie dies also als Richtlinie, nicht als wörtlichen Code):

    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
    

Ich beschreibe auch polymorphe Assoziationen in meiner Präsentation Praktische objektorientierte Modelle in SQL und meinem Buch SQL Antipatterns: Vermeiden der Fallstricke der Datenbankprogrammierung .


Zu Ihrem Kommentar: Ja, ich weiß, dass es eine weitere Spalte gibt, in der der Name der Tabelle vermerkt ist, auf die der Fremdschlüssel angeblich verweist. Dieses Design wird von Fremdschlüsseln in SQL nicht unterstützt.

Was passiert zum Beispiel, wenn Sie einen Kommentar einfügen und dafür "Video" als Namen der übergeordneten Tabelle nennen Comment? Es existiert keine Tabelle mit dem Namen "Video". Sollte die Einfügung mit einem Fehler abgebrochen werden? Welche Einschränkung wird verletzt? Woher weiß das RDBMS, dass diese Spalte eine vorhandene Tabelle benennen soll? Wie wird mit Tabellennamen umgegangen, bei denen die Groß- und Kleinschreibung nicht berücksichtigt wird?

Wenn Sie die EventsTabelle löschen, aber Zeilen enthalten, in Commentsdenen Ereignisse als übergeordnetes Element angegeben sind, was sollte das Ergebnis sein? Sollte die Drop-Tabelle abgebrochen werden? Sollten Zeilen Commentsverwaist sein? Sollten sie sich ändern, um auf eine andere vorhandene Tabelle zu verweisen, wie z Articles. Haben die ID-Werte, auf die früher verwiesen wurde, Eventsbeim Zeigen Sinn ergeben Articles?

Diese Dilemmata sind alle auf die Tatsache zurückzuführen, dass polymorphe Assoziationen von der Verwendung von Daten (dh einem Zeichenfolgenwert) abhängen, um auf Metadaten (einen Tabellennamen) zu verweisen. Dies wird von SQL nicht unterstützt. Daten und Metadaten sind getrennt.


Es fällt mir schwer, mich mit Ihrem Vorschlag "Concrete Supertable" zu beschäftigen.

  • Definieren Sie Commentableals echte SQL-Tabelle, nicht nur als Adjektiv in Ihrer Rails-Modelldefinition. Es sind keine weiteren Spalten erforderlich.

    CREATE TABLE Commentable (
      id INT AUTO_INCREMENT PRIMARY KEY
    ) TYPE=InnoDB;
    
  • Definieren Sie die Tabellen Articles, Photosund Eventsals „Unterklassen“ von Commentable, indem sie ihre Primärschlüssel auch ein Fremdschlüssel Referenzierung Commentable.

    CREATE TABLE Articles (
      id INT PRIMARY KEY, -- not auto-increment
      FOREIGN KEY (id) REFERENCES Commentable(id)
    ) TYPE=InnoDB;
    
    -- similar for Photos and Events.
    
  • Definieren Sie die CommentsTabelle mit einem Fremdschlüssel zu Commentable.

    CREATE TABLE Comments (
      id INT PRIMARY KEY AUTO_INCREMENT,
      commentable_id INT NOT NULL,
      FOREIGN KEY (commentable_id) REFERENCES Commentable(id)
    ) TYPE=InnoDB;
    
  • Wenn Sie Article(zum Beispiel) eine erstellen möchten , müssen Sie auch eine neue Zeile erstellen Commentable. So auch für Photosund 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(), ... );
    
  • Wenn Sie eine erstellen möchten Comment, verwenden Sie einen Wert, der in vorhanden ist Commentable.

    INSERT INTO Comments (id, commentable_id, ...)
    VALUES (DEFAULT, 2, ...);
    
  • Wenn Sie Kommentare zu einem bestimmten Thema abfragen möchten Photo, führen Sie einige Verknüpfungen aus:

    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;
    
  • Wenn Sie nur die ID eines Kommentars haben und herausfinden möchten, für welche kommentierbare Ressource es sich um einen Kommentar handelt. Aus diesem Grund ist es möglicherweise hilfreich, wenn die Tabelle "Kommentierbar" angibt, auf welche Ressource sie verweist.

    SELECT commentable_id, commentable_type FROM Commentable t
    JOIN Comments c ON (t.id = c.commentable_id)
    WHERE c.id = 42;
    

    Anschließend müssen Sie eine zweite Abfrage ausführen, um Daten aus der jeweiligen Ressourcentabelle (Fotos, Artikel usw.) abzurufen, nachdem Sie ermittelt haben, aus commentable_typewelcher Tabelle Sie beitreten möchten. Sie können dies nicht in derselben Abfrage tun, da SQL erfordert, dass Tabellen explizit benannt werden. Sie können keiner Tabelle beitreten, die durch Datenergebnisse in derselben Abfrage bestimmt wird.

Zugegeben, einige dieser Schritte verstoßen gegen die von Rails verwendeten Konventionen. Die Rails-Konventionen sind jedoch in Bezug auf das ordnungsgemäße Design relationaler Datenbanken falsch.

Bill Karwin
quelle
2
Vielen Dank für das Follow-up. Damit wir uns auf derselben Seite befinden, verwenden polymorphe Assoziationen in Rails zwei Spalten in unserem Kommentar für den Fremdschlüssel. Eine Spalte enthält die ID der Zielzeile, und die zweite Spalte teilt Active Record mit, in welchem ​​Modell sich dieser Schlüssel befindet (Artikel, Foto oder Ereignis). Würden Sie in diesem Wissen die drei von Ihnen vorgeschlagenen Alternativen empfehlen? Es fällt mir schwer, mich mit Ihrem Vorschlag "Concrete Supertable" zu beschäftigen. Was meinst du, wenn du sagst "Verknüpfe deine Kommentare mit dieser Supertabelle" (Kommentierbar)?
Eggdrop
1
Danke für die Erklärung. Ich glaube, ich verstehe, warum Sie sagen, dass die Rails-Konventionen in Bezug auf das ordnungsgemäße Design relationaler Datenbanken falsch sind. Das Muster ähnelt in gewisser Weise der Verwendung von Flatfiles als Speichermechanismus, da es die Fähigkeit verliert, verschiedene relationale Einschränkungen durchzusetzen.
Eggdrop
6
Genau. Es sollte ein starker "Codegeruch" sein, dass das relationale Datenbankdesign nicht korrekt ist, wenn die Dokumentation zu Polymorphic Associations selbst besagt, dass Sie keine Fremdschlüsseleinschränkungen verwenden können!
Bill Karwin
1
Ein Nachteil der Concrete Supertable-Lösung besteht darin, dass sie keine referenzielle Integrität für die untergeordnete Tabelle erzwingt. Zum Beispiel wäre es möglich, dass eine Ereigniszeile und eine Fotozeile dieselbe commentable_id haben. Zugegeben, die Verwendung einer guten Prozedur zum Erstellen der commentable_id und zum Zuweisen zu einer untergeordneten Tabelle sollte diese Situation vermeiden, aber die Möglichkeit besteht weiterhin.
Jason Martens
1
@ Mohamad, STI würde gut funktionieren. Sie können weiterhin Fremdschlüssel definieren, wenn Ihre übergeordnete Tabelle STI verwendet. Oder auch wenn die untergeordnete Tabelle STI verwendet.
Bill Karwin
3

Bill Karwin hat Recht, dass Fremdschlüssel nicht mit polymorphen Beziehungen verwendet werden können, da SQL nicht wirklich über ein natives Konzept polymorpher Beziehungen verfügt. Wenn Ihr Ziel, einen Fremdschlüssel zu haben, darin besteht, die referenzielle Integrität zu erzwingen, können Sie ihn über Trigger simulieren. Dies wird DB-spezifisch, aber im Folgenden sind einige kürzlich von mir erstellte Trigger aufgeführt, um das kaskadierende Löschverhalten eines Fremdschlüssels in einer polymorphen Beziehung zu simulieren:

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();

In meinem Code kann sich ein Datensatz in der brokeragesTabelle oder ein Datensatz in der agentsTabelle auf einen Datensatz in der subscribersTabelle beziehen .

Eric Anderson
quelle
Das ist toll. Irgendwelche Ideen, wie man einen ähnlichen Auslöser erstellen könnte, um sicherzustellen, dass neu erstellte polymorphe Assoziationen auf einen gültigen Typ und eine gültige ID verweisen?
Cayblood