¿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
foreign_key
opción a la que se puede pasarbelongs_to
. El OP está hablando de una "restricción de clave externa" de la base de datos nativa. Eso me confundió por un tiempo.Respuestas:
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
Events
tabla, pero tiene filasComments
que indican Eventos como su padre, ¿cuál debería ser el resultado? ¿Debe abortarse la tabla de caída? ¿DeberíanComments
quedar huérfanas las filas en ? ¿Deberían cambiar para hacer referencia a otra tabla existente comoArticles
? ¿Los valores de id que solían señalarEvents
tienen algún sentido al señalarArticles
?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.
Defínalo
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;
Defina las tablas
Articles
,Photos
yEvents
como "subclases" deCommentable
, haciendo que su clave principal sea también una referencia de clave externaCommentable
.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
Comments
tabla con una clave externa aCommentable
.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 enCommentable
. También paraPhotos
yEvents
.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 enCommentable
.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_type
qué 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.
fuente
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
brokerages
tabla o un registro en laagents
tabla puede relacionarse con un registro en lasubscribers
tabla.fuente