¿Por qué no puede tener una clave externa en una asociación polimórfica?

81

¿Por qué no puede tener una clave externa en una asociación polimórfica, como la que se representa a continuación como modelo de 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
huevo
fuente
3
Solo para aclarar a los demás, el OP no habla de la foreign_keyopción a la que se puede pasar belongs_to. El OP está hablando de una "restricción de clave externa" de la base de datos nativa. Eso me confundió por un tiempo.
Joshua Pinter

Respuestas:

178

Una clave externa debe hacer referencia solo a una tabla principal. Esto es fundamental tanto para la sintaxis SQL como para la teoría relacional.

Una asociación polimórfica es cuando una columna determinada puede hacer referencia a dos o más tablas principales. No hay forma de que pueda declarar esa restricción en SQL.

El diseño de asociaciones polimórficas rompe las reglas del diseño de bases de datos relacionales. No recomiendo usarlo.

Existen varias alternativas:

  • Arcos exclusivos: cree varias columnas de clave externa, cada una de las cuales hace referencia a un padre. Haga cumplir que exactamente una de estas claves externas puede ser no NULL.

  • Invertir la relación: utilice tres tablas de varios a varios, cada una de las cuales hace referencia a los comentarios y al padre respectivo.

  • Supertable concreto: en lugar de la superclase "comentable" implícita, cree una tabla real a la que haga referencia cada una de sus tablas principales. Luego, vincule sus Comentarios a esa supertabla. El código de pseudo-rails sería algo como lo siguiente (no soy un usuario de Rails, así que trátelo como una guía, no como un código literal):

    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
    

También cubro las asociaciones polimórficas en mi presentación Practical Object-Oriented Models in SQL y en mi libro SQL Antipatterns: Avoiding the Pitfalls of Database Programming .


Re su comentario: Sí, sé que hay otra columna que anota el nombre de la tabla a la que supuestamente apunta la clave externa. Este diseño no es compatible con claves externas en SQL.

¿Qué sucede, por ejemplo, si inserta un comentario y nombra "Video" como el nombre de la tabla principal para eso Comment? No existe ninguna tabla llamada "Video". ¿Debe abortarse la inserción con un error? ¿Qué restricción se está infringiendo? ¿Cómo sabe el RDBMS que se supone que esta columna nombra una tabla existente? ¿Cómo maneja los nombres de tablas que no distinguen entre mayúsculas y minúsculas?

Del mismo modo, si quita la Eventstabla, pero tiene filas Commentsque indican Eventos como su padre, ¿cuál debería ser el resultado? ¿Debe abortarse la tabla de caída? ¿Deberían Commentsquedar huérfanas las filas en ? ¿Deberían cambiar para hacer referencia a otra tabla existente como Articles? ¿Los valores de id que solían señalar Eventstienen algún sentido al señalar Articles?

Todos estos dilemas se deben al hecho de que las asociaciones polimórficas dependen del uso de datos (es decir, un valor de cadena) para hacer referencia a los metadatos (un nombre de tabla). Esto no es compatible con SQL. Los datos y los metadatos están separados.


Me está costando entender tu propuesta de "Concrete Supertable".

  • Defínalo Commentablecomo una tabla SQL real, no solo como un adjetivo en la definición de su modelo Rails. No se necesitan otras columnas.

    CREATE TABLE Commentable (
      id INT AUTO_INCREMENT PRIMARY KEY
    ) TYPE=InnoDB;
    
  • Defina las tablas Articles, Photosy Eventscomo "subclases" de Commentable, haciendo que su clave principal sea también una referencia de clave externa Commentable.

    CREATE TABLE Articles (
      id INT PRIMARY KEY, -- not auto-increment
      FOREIGN KEY (id) REFERENCES Commentable(id)
    ) TYPE=InnoDB;
    
    -- similar for Photos and Events.
    
  • Defina la Commentstabla con una clave externa a Commentable.

    CREATE TABLE Comments (
      id INT PRIMARY KEY AUTO_INCREMENT,
      commentable_id INT NOT NULL,
      FOREIGN KEY (commentable_id) REFERENCES Commentable(id)
    ) TYPE=InnoDB;
    
  • Cuando desee crear un Article(por ejemplo), también debe crear una nueva fila en Commentable. También para Photosy 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(), ... );
    
  • Cuando desee crear un Comment, use un valor que exista en Commentable.

    INSERT INTO Comments (id, commentable_id, ...)
    VALUES (DEFAULT, 2, ...);
    
  • Cuando desee consultar los comentarios de un determinado Photo, realice algunas combinaciones:

    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;
    
  • Cuando solo tiene la identificación de un comentario y desea encontrar para qué recurso comentable es un comentario. Para esto, puede resultarle útil que la tabla de comentarios designe a qué recurso hace referencia.

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

    Luego, deberá ejecutar una segunda consulta para obtener datos de la tabla de recursos respectiva (fotos, artículos, etc.), después de descubrir desde commentable_typequé tabla unirse. No puede hacerlo en la misma consulta, porque SQL requiere que las tablas se nombren explícitamente; no puede unirse a una tabla determinada por los resultados de los datos en la misma consulta.

Es cierto que algunos de estos pasos rompen las convenciones utilizadas por Rails. Pero las convenciones de Rails son incorrectas con respecto al diseño adecuado de bases de datos relacionales.

Bill Karwin
fuente
2
Gracias por hacer un seguimiento. Para que estemos en la misma página, en Rails, las asociaciones polimórficas usan dos columnas en nuestro Comentario para la clave externa. Una columna contiene la identificación de la fila de destino, y la segunda columna le dice a Active Record en qué modelo se encuentra esa clave (artículo, foto o evento). Sabiendo esto, ¿recomendaría las tres alternativas que ha propuesto? Me está costando entender tu propuesta de "Concrete Supertable". ¿A qué te refieres cuando dices "vincular tus comentarios a esa supertabla" (comentable)?
eggdrop
1
Gracias por explicarlo. Creo que entiendo por qué dice que las convenciones de Rails están equivocadas con respecto al diseño adecuado de la base de datos relacional: el patrón de alguna manera se asemeja al uso de archivos planos como mecanismo de almacenamiento, ya que pierde la capacidad de hacer cumplir varias restricciones relacionales.
eggdrop
6
Exactamente. Debería ser un fuerte "olor a código" que no es un diseño de base de datos relacional correcto cuando la documentación de las asociaciones polimórficas dice que no se pueden usar restricciones de clave externa.
Bill Karwin
1
Una desventaja de la solución Concrete Supertable es que no impone la integridad referencial en la mesa de los niños. Por ejemplo, sería posible que una fila de Eventos y una fila de Fotos tuvieran el mismo commentable_id. Por supuesto, usar un buen procedimiento para crear el commentable_id y asignarlo a una tabla secundaria debería evitar esta situación, pero la posibilidad aún existe.
Jason Martens
1
@Mohamad, STI funcionaría bien. Aún puede definir claves externas si su tabla principal usa STI. O incluso si la tabla secundaria usara ITS.
Bill Karwin
3

Bill Karwin tiene razón en que las claves externas no se pueden usar con relaciones polimórficas debido a que SQL no tiene realmente un concepto nativo de relaciones polimórficas. Pero si su objetivo de tener una clave externa es hacer cumplir la integridad referencial, puede simularla mediante disparadores. Esto se vuelve específico de DB, pero a continuación se muestran algunos activadores recientes que creé para simular el comportamiento de eliminación en cascada de una clave externa en una relación polimórfica:

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

En mi código, un registro en la brokeragestabla o un registro en la agentstabla puede relacionarse con un registro en la subscriberstabla.

Eric Anderson
fuente
Esto es genial. ¿Alguna idea sobre cómo se podría crear un disparador similar para asegurarse de que las asociaciones polimórficas recién creadas apunten a un tipo e ID válidos?
cayblood