accepts_nested_attributes_forを使わず、複数の子レコードを保存する

この記事は Money Forward Advent Calendar 2018 15日目の記事です。

こんにちは、マネーフォワードでマネーフォワードクラウドの開発をしている @kamille です。
今年の10月に新卒で入社して、今はマネーフォワードクラウドの全体的な課題を解決する横断チームにてRailsエンジニアをしています。
今日はRailsの accepts_nested_attributes_for メソッドについてお話したいと思います。

はじめに

突然ですが皆さん accepts_nested_attributes_for は好きですか?僕は複数リリースの同時保存・更新が超お手軽にできて素敵なコンセプトだなと思います。
でもこのメソッドはあまり評判が良くなく、特にRails生みの親のDHHがaccepts_nested_attributes_forを消したいと言っていたり、代わりとなる公式なformオブジェクトのプロジェクトが立ち上がったり(残念ながら途中でなくなってしまったようです 😥 )している等、あまりいいメソッドと思われていないようです。

社内でも accepts_nested_attributes_for は今後は使わないようにして、既存のコードもリプレイスしていく活動が始まっているので accepts_nested_attributes_for を使わずに、 FormObject を使って複数リリースの同時保存を行うコードを書いてみました。

Model

データ構造としては、Companyに紐づく、 Employee , President があるとします。
それぞれ has_manyhas_one の関係で Company リソースと紐付いていて、Companyを登録する時に2つの子リソースも同時に登録したいものとします。

class Company
  has_many :employees
  has_one :president
end

class Employee
  belongs_to :company
end

class President
  belongs_to :company
end

ControllerとView

FormObjectのインスタンスをviewに渡して、form_withの引数にその変数を渡しました。
ここはモデルのインスタンスを渡している普段の使い方がフォームのインスタンスになっただけです。(フォームのインスタンスが form_with の引数に使えるのは ActiveModel::Model というAPIをFormObject内でインクルードしているからです)

class RegistrationsController
  def new
    @company_registration_form = CompanyRegistrationForm.new
  end

  def create
    @company_registration_form = CompanyRegistrationForm.new(company_registration_form_params)

    if @company_registration_form.save
      # 成功
    else
      # 失敗
    end
  end
  ...

  private

  def company_registration_form_params
    params.require(:company_registration_form_params).permit(
      name:,
      address:,
      employees_attributes: [:first_name, :last_name, :job_type],
      president_attributes: [:first_name, :last_name]
    )
  end
end

Viewでは fields_for を使ってEmployeeとPresidentリソースも操作できるようにしています。

<%= form_with @company_registration_form do |form| %>
  First name: <%= form.text_field :first_name %>
  Last name : <%= form.text_field :last_name %>

  <%= form.fields_for :employees do |employee_fields| %>
    First name: <%= employee_fields.text_field :first_name %>
    Last name : <%= employee_fields.text_field :last_name %>
    job_type : <%= employee_fields.text_field :job_type %>
  <% end %>

  <%= form.fields_for :president do |president_fields| %>
    First name: <%= president_fields.text_field :first_name %>
    Last name : <%= president_fields.text_field :last_name %>
  <% end %>

  <%= form.submit %>
<% end %>

FormObject

ActiveModel::Model をインクルードして、このクラスのインスタンスを form_with の引数に使えるようにしたり、このFormObjectを使用するコンテキスト時だけ適用したいバリデーションなどを追加することができます。モデルのバリデーションだとすべてのコンテキストでバリデーションが動いてしまうのでこれは便利です。

class CompanyRegistrationForm
  include ActiveModel::Model

  concerning :CompanyBuilder do
    def company
      @company ||= Company.new
    end
  end

  concerning :EmployeesBuilder do
    attr_reader :employees_attributes

    def employees
      @employees_attributes ||= Employee.new
    end

    def employees_attributes=(attributes)
      @employees_attributes = Employee.new(attributes)
    end
  end

  concerning :PresidentBuilder do
    attr_reader :president_attributes

    def president
      @president_attributes ||= president.new
    end

    def employees_attributes=(attributes)
      @president_attributes = President.new(attributes)
    end
  end

  attr_accessor :name, :address

  validate :validate_something

  def save
    # バリデーションに引っかかる場合は以降の処理には行かせずfalseをコントローラーに返します
    return false if invalid?

    company.assign_attributes(company_params)
    build_asscociations

    if company.save
      true
    else
      false
    end
  end

  private

  def company_params
    {
      name: name,
      address: address
    }
  end

  def build_asscociations
    company.employees << employees
    company.president = president
  end

  def validate_something
    # Do something
  end
end

最後に

accepts_nested_attributes_for を使わない方法はいかがでしたでしょうか。

明日は jjjjjiiiiinnnnn さんが、「価値とUIについて」の記事を公開します。
価値って聞くとマネーフォワードっぽいですね! 笑 それでは!

 
 
マネーフォワードでは、エンジニアを募集しています。
ご応募お待ちしています。

【採用サイト】
マネーフォワード採用サイト
Wantedly | マネーフォワード

【マネーフォワードのプロダクト】
自動家計簿・資産管理サービス『マネーフォワード ME』 iPhone,iPad Android

「しら」ずにお金が「たま」る 人生を楽しむ貯金アプリ『しらたま』 iPhone,iPad

おトクが飛び出すクーポンアプリ『tock pop トックポップ』

金融商品の比較・申し込みサイト『Money Forward Mall』

くらしの経済メディア『MONEY PLUS』

■ビジネス向けクラウドサービス『マネーフォワードクラウドシリーズ』
バックオフィス業務を効率化『マネーフォワードクラウド』
会計ソフト『マネーフォワードクラウド会計』
確定申告ソフト『マネーフォワードクラウド確定申告』
請求書管理ソフト『マネーフォワードクラウド請求書』
給与計算ソフト『マネーフォワードクラウド給与』
経費精算ソフト『マネーフォワードクラウド経費』
マイナンバー管理ソフト『マネーフォワードクラウドマイナンバー』
資金調達サービス『マネーフォワードクラウド資金調達』

Pocket