Relaciones polimórficas

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.

Forma tradicional

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.

Relación polimorfica

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

Paso a paso

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.

Modelo Comment

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

Relaciones

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

Comments controller

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]

Vistas

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

Validación

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.

Conclusión

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.


Comentarios

hace 7 months

esto es una prueba de comentario