Polymorphic Association in Rails 5

Một trong những chủ đề mà các bạn mới bắt đầu với rails thường gặp khó khăn là Polymorphic Association trong Rails, bài viết sau hướng dẫn các bạn cách viết chức năng comment cho các object khác nhau dùng Polymorphic Association.

Polymorphic Associations là gì?

Quan hệ đa hình, khái niệm của quan hệ này khá đơn giản: bạn có một model có thể thuộc về nhiều model khác nhau trong một liên kết duy nhất, (a model can belong to more than one other model, on a single association)

Starter Rails 5 App

Giả sử chúng ta có 3 model article, event và photo. Bây giờ chúng ta muốn tạo comment cho article, event và photo. Comment khá tương tự, ngoại trừ việc chúng thuộc vào những model khác nhau. Đây là lúc Polymorphic Associations phát huy tác dụng.

Creat new Rails 5 project

rails new polym_demo

Chúng ta tạo các model (article, event và photo):

    rails g model article name content:text
    rails g model event name starts_at:datetime ends_at:datetime description:text
    rails g model photos name filename 

Chúng ta tạo các table bằng cách chạy lệnh:

rails db:migrate

Chúng ta tạo các controller:

rails g controller articles
rails g controller events
rails g controller photos

Define the resources in routes.rb

Rails.application.routes.draw do
  resources :photos
  resources :events
  resources :articles

  root to: 'articles#index'
end

Polymorphic Association Tạo model comment

rails g model comment content:text commentable_id:integer:index commentable_type:index

commentable_id chính là foreign key để thết lập quan hệ với các table khác. Dần dần, commentable_type sẽ chứa tên thật của model (có comment tương ứng). Migration:

class CreateComments < ActiveRecord::Migration[5.0]
  def change
    create_table :comments do |t|
      t.text :content
      t.integer :commentable_id
      t.string :commentable_type

      t.timestamps
    end
    add_index :comments, :commentable_id
    add_index :comments, :commentable_type
  end
end

Có thể viết lại thành:

class CreateComments < ActiveRecord::Migration[5.0]
  def change
    create_table :comments do |t|
      t.text :content
      t.belongs_to :commentable, polymorphic: true

      t.timestamps
    end
    add_index :comments, :commentable_id
    add_index :comments, :commentable_type
  end
end

Comment model sẽ có liên kết belongs_to, nhưng với một thay đổi nhỏ:

belongs_to :commentable, polymorphic: true    

Add

has_many :comments, as: :commentable

vào các model article, event và photo. :as là một tùy chọn đặc biệt, giải thích rằng “đây là liên kết đa hình. Giờ, hãy boot console và thử chạy:

a = Article.create({name: 'test', content: 'this is test'})
a.comments.create({content: "this is comment"})

kết quả

=> #<Comment id: 1, content: "this is comment", commentable_id: 1, commentable_type: "Article", created_at: "2016-12-29 23:06:18", updated_at: "2016-12-29 23:06:18"> 

giờ chúng ta đã có thể sử dụng polymorphic association.

Tạo cotroller comments

rails g controller comments index new

Define the nested resources in routes.rb.

Rails.application.routes.draw do
  resources :photos do
    resources :comments
  end

  resources :events do
    resources :comments
  end

  resources :articles do
    resources :comments
  end

  root to: 'articles#index'
end

code file comments_controller.rb

class CommentsController < ApplicationController
  before_action :load_commentable

  def index
    @comments = @commentable.comments
  end

  def new
    @comment = @commentable.comments.new
  end

  def create
    @comment = @commentable.comments.new comment_params
    if @comment.save
      redirect_to @commentable, notice: "Comment created."
    else
      render :new
    end
  end

  private
  def load_commentable
    resource, id = request.path.split('/')[1,2]
    @commentable = resource.singularize.classify.constantize.find(id)
  end
  def comment_params
    params.require(:comment).permit!
  end

end

code file articles_controller.rb

class ArticlesController < ApplicationController
  before_action :load_article, only: [:show, :edit, :update, :destroy]
  def index
    @articles = Article.all
  end

  def show
    @commentable = @article
    @comments = @commentable.comments
    @comment = Comment.new
  end

  def new
    @article = Article.new
  end

  def edit
  end

  def create
    @article = Article.new article_params
    if @article.save
      redirect_to @article, notice: "Article was successfully created."
    else
      render :new
    end
  end

  def update
    if @article.update_attributes(params[:article])
      redirect_to @article, notice: "Article was successfully updated."
    else
      render :edit
    end
  end

  def destroy
    @article.destroy
    redirect_to articles_url, notice: "Article was destroyed."
  end

  private

  def load_article
    @article = Article.find(params[:id])
  end

  def article_params
    binding.pry
    params.require(:article).permit!
  end
end

Trong view chúng ta sẽ có các file sau file comments/_comments.html.erb

<div id="comments">
<% @comments.each do |comment| %>
  <div class="comment">
    <%= simple_format comment.content %>
  </div>
<% end %>
</div>

file comments/_form.html.erb

<%= form_for [@commentable, @comment] do |f| %>
  <% if @comment.errors.any? %>
    <div class="error_messages">
      <h2>Please correct the following errors.</h2>
      <ul>
      <% @comment.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.text_area :content, rows: 8 %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

file comments/index.html.erb

<h1>Comments</h1>
<%= render 'comments' %>
<p><%= link_to "New Comment", [:new, @commentable, :comment] %></p>

file comments/new.html.erb

<h1>New Comment</h1>
<%= render 'form' %>

file articles/_form.html.erb

<%= form_for(@article) do |f| %>
  <% if @article.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>

      <ul>
      <% @article.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :content %><br />
    <%= f.text_area :content %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

file articles/edit.html.erb

<h1>Editing article</h1>

<%= render 'form' %>

<%= link_to 'Show', @article %> |
<%= link_to 'Back', articles_path %>

file articles/index.html.erb

<h1>Articles</h1>

<div id="articles">
<% @articles.each do |article| %>
  <h2><%= link_to article.name, article %></h2>
  <div class="content"><%= simple_format(article.content) %></div>
<% end %>
</div>
<p><%= link_to "New Article", new_article_path %></p>

file articles/new.html.erb

<h1>New article</h1>

<%= render 'form' %>

<%= link_to 'Back', articles_path %>

file articles/show.html.erb

<h1><%= @article.name %></h1>

<%= simple_format @article.content %>

<p><%= link_to "Back to Articles", articles_path %></p>

<h2>Comments</h2>

<%= render "comments/comments" %>
<%= render "comments/form" %>

Chạy rails s để kiểm tra kết quả

  • chúng ta tạo new article

  • chúng ta test chức năng comment article, kết quả hình bên dưới Các chức năng comment cho Event và Photo tương tự.

Kết Luận

Đây là một ví dụ cơ bản của Polymorphic Associations . Hi vọng nó sẽ giúp được cho các bạn mới bắt đầu để làm việc tốt hơn với Rails.