12/08/2018, 11:35

Form object, giải pháp thay thế cho Active Record Nested Attributes

Xin chào các bạn Chắc hẳn những ai sử dụng Rails cũng đã đều quen thuộc với `accepts_nested_attributes_for` để xử lý Nested Attributes. Tuy nhiên, khi ứng dụng của bạn lớn, việc sử dụng accepts_nested_attributes_for khiến cho Model ngày càng trở nên rối, dần dần dẫn đến tình trạng "fat model, ...

Xin chào các bạn
Chắc hẳn những ai sử dụng Rails cũng đã đều quen thuộc với `accepts_nested_attributes_for` để xử lý Nested Attributes.

Tuy nhiên, khi ứng dụng của bạn lớn, việc sử dụng accepts_nested_attributes_for khiến cho Model ngày càng trở nên rối, dần dần dẫn đến tình trạng "fat model, skinny controller".

Bởi vậy hôm nay tôi sẽ trình bày một trong nhưng cách để giải quyết vấn đề này - Form Object

- Cảm ơn anh DieuNB đã suggest cho em kỹ thuật này (lay2) -

1. Form object là gì?

Form object là kỹ thuật tạo ra một class trung gian mang các attributes của những model cần tác động.

Tại Controller, thay vì khởi tạo đối tượng trực tiếp từ Model, ta sẽ gọi tới class trung gian này.

Việc sử dụng Form object giúp bạn giảm thiểu tình trạng "Fat model", đồng thời giúp code sáng sủa hơn, validates dễ dàng hơn.

Trên thực tế, nhiều lập trình viên có kinh nghiệm cũng phản đối việc sử dụng accepts_nested_attributes_for và dự đoán nó sẽ bị loại bỏ ở các bản Rails sau này (yaoming)

2. Sử dụng

Bài toán đặt ra:

  • Khi người dùng signup, các thông tin cần nhập vào là username, email, address, github.
  • username, email được lưu trong table Users. address, github được lưu trong table Profiles.
  • User và Profile có quan hệ 1-1.
  • Chỉ sử dụng 1 form.

Ta có 2 Model với quan hệ như sau:

class User < ActiveRecord::Base
  has_one :profile
end
class Profile < ActiveRecord::Base
  belong_to :user
end

Model User có trường: :username, :email

Model Profile có trường: :address, :github

Thay vì khai báo accepts_nested_attributes_for :profile trong model User, ta tạo ra 1 class mới đặt trong folder app/forms

class Signup
  include ActiveModel::Model

  attr_accessor :username, :email, :address, :github

  validates :address, presence: true
  validates :email, format: {with: /A([^@s]+)@((?:[-a-z0-9]+.)+[a-z]{2,})z/i, on: :create}
  validates :github, presence: true
  validates :username, presence: true

  def register
    if valid?
      create_user
      create_profile
    end
  end

  private
  def create_user
    hash = Hash.new
    hash[:username] = "#{username}"
    hash[:email] = "#{email}"
    @user ||= User.new hash
    @user.save!
  end

  def create_profile
    hash = Hash.new
    hash[:address] = "#{address}"
    hash[:github] = "#{github}"
    @profile ||= Profile.new hash
    @profile.user = @user
    @profile.save!
  end
end

Ta include ActiveModel::Model để class có thể sử dụng các methods trong Model.

attr_accessor get, set cho các field sẽ sử dụng và xuất hiện trên form.

Method register dùng để create User và Profile tương ứng của nó. Method sẽ được gọi từ SignUpsController.

class SignupsController < ApplicationController
  def new
    @signup = Signup.new
  end

  def create
    @signup = Signup.new signup_params
    @signup.register
  end

  private
  def signup_params
    params.require(:signup).permit :username, :email, :address, :github
  end
end

Tại `View` ta sử dụng `form_for` với object `@signup` được khai báo bên phía Controller như bình thường:
<%= form_for(@signup) do |f| %>
  <% if @signup.errors.any? %>
    <ul>
      <% @signup.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  <% end %>
  <%= f.label :username %>
  <%= f.text_field :username %>

  <%= f.label :email %>
  <%= f.text_field :email %>

  <%= f.label :address %>
  <%= f.text_field :address %>

  <%= f.label :github %>
  <%= f.text_field :github %>

  <%= f.submit %>
<% end %>

Tham khảo

  • 7 Patterns to Refactor Fat ActiveRecord Models:

    http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

Cảm ơn các bạn đã theo dõi (lay2)

0