FormObjectの実装
はじめに
業務でFormObjectを使って実装したので、サンプルコードを使って実装の要点をまとめる。
間違ってる点などあれば、指摘していただけると幸いだ。
状況
Userクラスのnameとemailと、Userクラスがhistoryクラスにdelegateしているorder_historyをUserControllerのupdateで更新したい。
ただ、何個の値が更新されるかわからない状況だ。(全て更新されるかもしれないし、一つしか更新されないかもしれない。)
(実際のプロジェクトでこんなモデルやクラスは作らなそうではあるが、今回あくまでサンプルなのでお目溢し願う)
実装
# frozen_string_literal: true class User class UpdateForm include ActiveModel::Model include ActiveModel::Attributes attr_accessor :user attribute :name attribute :email attribute :order_history def initialize(user, attributes = {}) @user = user default_attributes = { name: user.name, email: user.email, order_history: user.order_history, } super(default_attributes.merge(attributes)) end def save! ActiveRecord::Base.transaction do user.update!(attributes.except("order_history")) user.history.update!(order_history: order_history) end end delegate :history, to: :user end end
解説
include ActiveModel::Model
これによってvalidationやコールバックなどの機能が使えるようになる。
include ActiveModel::Attributes
これによってattributeメソッドが使えるようになる。
attributeを使うことでクラス属性の型を意識しなくても指定の型へ変換してくれている。
またこれを書くことで型を明示することもできる。
↓下記ページ参照させていただきました。 ushinji.hatenablog.com
attr_accessor :user
attr_accessorは読み取りも書き込みもできるオブジェクトの属性を定義したい時に使う。
ちなみに attr_reader は読み出し専用の属性を定義したいときに使い、
attr_writer は書き込み専用の属性を定義したいときに使う。
def initialize(user, attributes = {}) @user = user default_attributes = { name: user.name, email: user.email, order_history: user.order_history, } super(default_attributes.merge(attributes)) end
このsuperはActiveModel::Attributesのinitializeメソッドをよんでいて、
これによってattributeメソッドが使えるようになる。
この時ActiveModel::Modelのinitializeメソッドは呼ばれていない。(はず)
なぜなら、Rubyでは同じ名前のメソッドを持つ複数モジュールをあるクラスでincludeし、
それらをsuperで呼び出した際、
後にincludeした方のモジュールのメソッドがそのクラスでは採用されるようになっているからだ。
例:
module Hello def greet puts 'Hello!!' end end module Ola def greet puts 'Ola!!' end end class Greeting include Hello include Ola def greet super end end g = Greeting.new g.greet # =>"Ola!!"
なのでこのsuperはActiveModel::Attributesのinitializeメソッドだけを呼んでいる。
default_attributes = { name: user.name, email: user.email, order_history: user.order_history, } super(default_attributes.merge(attributes))
ここでattributesを分けているのは、デフォルトの値を先に入れておいて、
mergeメソッドでコントローラーから渡されたattributesのhashをマージして、
変更箇所だけが変わったhashを作るため。
def save! ActiveRecord::Base.transaction do user.update!(attributes.except("order_history")) history.update!(order_history: order_history) end end
updateする時は上記のように変更したいインスタンスをそれぞれアップデートする。 userをupdateする時は、exceptでorder_historyを除外したhashを引数に渡す。
以上。