En las relaciones del tipo has_many
<=> belongs_to
nos permite asociar por ejemplo 1 categoría a N artículos, siendo categoría y artículo el mismo tipo de objetos. Pero a veces nos interesa que un objeto esté asociado en la misma relación con distinto tipo de objetos.
Por ejemplo el caso de los comentarios. Podemos imaginas una aplicación con los modelos Page
y Post
y ambos tendrían has_many :comments
.
Si lo hiciésemos de la forma tradicional tendríamos que crear una relación con sus respectivas columnas en comment
. Es decir:
class Page
has_many :comments
end
class Post
has_many :comments
end
class Comment
belongs_to :page
belongs_to :post
end
# schema.rb
create_table :comments do |t|
t.string :username
t.string :message
t.integer :page_id
t.integer :post_id
end
Si quisiéramos relacionar Comment
a más modelos tendríamos que seguir añadiendo relaciones y columnas a la table comments
.
En vez de eso podemos usar una relación polimórfica.
class Comment
belongs_to :commentable, polymorphic: true
end
class Page
has_many :comments, as: :commentable
end
class Post
has_many :comments, as: :commentable
end
# schema.rb
create_table :comments do |t|
t.string :username
t.string :message
t.string :commentable_type
t.integer :commentable_id
end
Para el ejemplo partiremos de un proyecto que tiene los modelos Post
y Page
creados.
https://github.com/tutoronrails/demo-commentable
Además usaremos haml y simple_form.
Para tu comodidad he creado tags por cada paso por si te pierdes.
Con el generador de rails creamos el modelo y la migración que necesitamos:
$ rails generate model Comment commentable:references{polymorphic}:index username:string message:text
commentable:references{polymorphic}
significa que tendrá una relación llamada commentable y que será polimórfica, nos debería de crear una migración parecida a esta:
class CreateComments < ActiveRecord::Migration[5.0]
def change
create_table :comments do |t|
t.references :commentable, polymorphic: true
t.string :username
t.text :message
t.timestamps
end
end
end
Si lanzamos la migración:
$ rake db:migrate
Podemos ver en el schema que nos ha creado los campos commentable_type
y commentable_id
y un índice con los dos:
# db/schema.rb
create_table "comments", force: :cascade do |t|
t.string "commentable_type"
t.integer "commentable_id"
t.string "username"
t.text "message"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["commentable_type", "commentable_id"], name: "index_comments_on_commentable_type_and_commentable_id", using: :btree
end
Si revisas el modelo será:
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
Ahora tenemos que añadir la relación a los modelos Page
y Post
:
class Page < ApplicationRecord
has_many :comments, as: :commentable
end
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
Nuestro CommentsController
solo va a necesitar el método create
:
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
@comment = Comment.new(comment_params)
if @comment.save
redirect_to [@comment.commentable], notice: 'Comment created'
else
render :new
end
end
private
def comment_params
params.require(:comment).permit(:username, :message, :commentable_id, :commentable_type)
end
end
Quizás te extrañe redirect_to [@comment.commentable]
, por debajo está haciendo uso de polymorphic_path
.
Por lo que sería lo mismo que redirect_to polymorphic_path([@comment.commentable])
a partir del tipo de objeto es capaz de inferir la ruta. Esto mismo se puede usar en los links y formulario simplificando mucho algunas cosas, eso sí a costa de rendimiento por lo que debe usarse con mesura.
También añadimos una nueva ruta:
# config/routes.rb
resources :comments, only: [:create]
Para listar y crear los comentarios vamos a usar dos partials y una vista:
# app/views/comments/new.html.haml
%h1= @comment.commentable.to_s
= render 'comments/form', comment: @comment
# app/views/comments/_form.html.haml
= simple_form_for [comment] do |f|
= f.input :commentable_id, as: :hidden
= f.input :commentable_type, as: :hidden
= f.input :username
= f.input :message
= f.submit 'comentar', class: 'btn'
Como puedes ver en el formulario de nuevo estamos haciendo uso del polymorphic_path
, es muy util en los comentarios pues te ahorra especificar el path y además es capaz de deducir si es update
o un create
.
# app/views/comments/_widget.html.haml
%hr
%h2 Comments
- commentable.comments.each do |comment|
%div{id: "comment-#{comment.to_param}"}
.row
.col-sm-2
%strong= comment.username
.col-sm-8
= link_to "#comment-#{comment.to_param}" do
%span.glyphicon.glyphicon-link
= time_ago_in_words(comment.created_at)
ago
%p= comment.message
= render 'comments/form', comment: commentable.comments.build
Ahora solo nos falta añadir un partial a pages#show
y posts#show
:
# app/views/pages/show.html.haml
= render 'comments/widget', commentable: @page
# app/views/posts/show.html.haml
= render 'comments/widget', commentable: @post
Para rematar podemos añadir una validaciones al modelo Comment
para comprobar qué pasa cuando falla.
# app/models/comment.rb
class Comment < ApplicationRecord
validates :username, presence: true
validates :message, presence: true
belongs_to :commentable, polymorphic: true
end
Si haces la prueba renderizará la vista new
y mostrará los errores.
Como puedes ver las relaciones polimórficas junto con algunos métodos de Rails nos pueden simplificar mucho las cosas. Pero debes tener en cuenta que casi todo lo que parece "mágico" tiene un precio y en este caso es el rendimiento.
esto es una prueba de comentario
dev|dedev