¿Por qué no puedes tener una clave foránea en una asociación polimórfica?


¿Por qué no puede tener una clave foránea en una asociación polimórfica, como la representada a continuación como un 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
Author: eggdrop, 2009-05-28

2 answers

Una clave foránea solo debe hacer referencia 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 dada puede hacer referencia a dos o más tablas principales. No hay manera 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.

Hay varios alternativas:

  • Arcos exclusivos: Crea múltiples columnas de clave foránea, cada una haciendo referencia a un padre. Haga cumplir que exactamente una de estas claves foráneas puede ser no NULA.

  • Invierta la Relación: Use tres tablas de muchos a muchos, cada una hace referencia a los Comentarios y a un padre respectivo.

  • Supertable concreto: En lugar de la superclase implícita "comentable", cree una tabla real que cada uno de sus padres referencias de tablas. Luego enlaza tus comentarios a esa supertable. El código pseudo-rails sería algo como lo siguiente (no soy un usuario de Rails, así que trata esto 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 mi libro SQL Antipatterns: Avoiding the Pitfalls of Database Programming.


Re su comentario: Sí, sé que hay otro columna que señala el nombre de la tabla a la que supuestamente apunta la clave foránea. Este diseño no es compatible con las claves foráneas en SQL.

¿Qué sucede, por ejemplo, si inserta un comentario y el nombre "Video" como el nombre de la tabla principal para eso Comment? No existe una tabla llamada "Video". ¿Se debe abortar la inserción con un error? ¿Qué restricción se está violando? ¿Cómo sabe el RDBMS que se supone que esta columna debe nombrar una tabla existente? ¿Cómo se maneja sin distinción de mayúsculas y minúsculas ¿nombres de tablas?

Del mismo modo, si se elimina la tabla Events, pero tiene filas en Comments que indican los eventos como su padre, ¿cuál debería ser el resultado? ¿Se debe abortar la tabla de caída? ¿Deberían las filas de Comments quedar huérfanas? ¿Deberían cambiar para referirse a otra tabla existente como Articles? ¿Los valores id que solían apuntar a Events tienen algún sentido cuando apuntan a Articles?

Estos dilemas se deben al hecho de que las Asociaciones polimórficas dependen del uso de datos (es decir, string value) para referirse a metadatos (un nombre de tabla). Esto no es compatible con SQL. Los datos y los metadatos son independientes.


Estoy teniendo dificultades para envolver mi cabeza alrededor de su propuesta de "Supertable de concreto".

  • Defina Commentable como 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;
    
  • Definir las tablas Articles, Photos, y Events como "subclases" de Commentable, haciendo su clave primaria es también una clave foránea que hace referencia a 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 tabla Comments con una clave foránea 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. Así también para Photos y 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, utilice un valor que exista en Commentable.

    INSERT INTO Comments (id, commentable_id, ...)
    VALUES (DEFAULT, 2, ...);
    
  • Cuando desee consultar comentarios de un Photo dado, haga algunos une:

    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 tienes el id de un comentario y quieres encontrar para qué recurso comentable es un comentario. Para esto, puede encontrar que es útil que la tabla comentable 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;
    

    Entonces tendría que ejecutar una segunda consulta para obtener datos de la tabla de recursos respectiva (Fotos, Artículos, etc.), después de descubrir desde commentable_type a qué tabla unirse. No puede hacerlo en la misma consulta, porque SQL requiere que las tablas sean nombradas explícitamente; no puede unirse a una tabla determinada por los resultados de datos en la misma consulta.

Es cierto que algunos de estos pasos rompen las convenciones utilizadas por Rails. Pero las convenciones de Rails están equivocadas con respecto al diseño adecuado de la base de datos relacional.

 153
Author: Bill Karwin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-19 18:16:51

Bill Karwin tiene razón en que las claves foráneas 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 foránea es reforzar la integridad referencial, puede simularla a través de disparadores. Esto hace que la base de datos sea específica, pero a continuación se muestran algunos disparadores recientes que creé para simular el comportamiento de eliminación en cascada de una clave foránea 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 tabla brokerages o una el registro en la tabla agents puede relacionarse con un registro en la tabla subscribers.

 0
Author: Eric Anderson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-11-08 15:22:36