【Rails】Form Object を作って Fat Model 解消に近づく
題名の通り、Form Object ですが、こんな感じで実装してるよってのを紹介したいと思います。
下記の例では GeneralUser モデルというユーザー名属性を持ったモデルと(他の属性は下記のForm Object では扱いません。)
GeneralUserAccount という銀行口座情報を格納するためのモデルを用いて説明します。
general_user が general_user_account を has_one している状態です。
まず Form Object です。
class AccountForm include Virtus.model include ActiveModel::Model attr_accessor :general_user, :general_user_account, :username def initialize(general_user) @general_user = general_user @general_user.build_general_user_account unless @general_user.general_user_account self.attributes = @general_user.attributes self.general_user_account = @general_user.general_user_account if @general_user.general_user_account end def assign_attributes(params = {}) @params = params general_user.assign_attributes(general_user_params) general_user.general_user_account.assign_attributes(general_user_account_params) end def save if !general_user.save add_errors(general_user) return false end true end private def general_user_params @params.require(:account_form).permit(:username) end def general_user_account_params @params.require(:account_form).require(:general_user_account).permit(:financial_institution_name, :branch_name, :branch_code, :account_number, :account_title, :account_type) end def add_errors(obj) obj.errors.messages.keys.each do |key, value| obj.errors.messages[key].each do |msg| errors.add(key, msg) end end end end
ポイントは何点かあるんだけど、まず
self.attributes = @general_user.attributes
の部分。
これはVirtus の機能なんだけど AccountForm クラスのインスタンスに GeneralUserモデルの属性をぶちこみます。
つまり
account_form = AccountForm.new
としたら
account_form.username
でGeneralUserモデルの属性にアクセスできる状態になる。
has_one している GeneralUserAccount モデルのほうは
self.general_user_account = @general_user.general_user_account if @general_user.general_user_account
という風に直接ぶちこむ。 こうすることで
普通にModelをnewしてform_for や form_fields に渡すのと同じように扱い、値を展開できる。
コントローラは以下の感じ。
def edit @account_form = AccountForm.new(current_user) end def update @account_form = AccountForm.new(current_user) @account_form.assign_attributes(params) if !@account_form.save flash.now[:alert] = '編集に失敗しました。' render action: 'edit' return end redirect_to ({:action => 'edit'}), :notice => '編集しました。' end
普通にmodel を使うような形でキレイに書けます。
それで View は以下
= form_for @account_form, url: {action: 'update'} do |f| = f.fields_for :general_user_account, f.object.general_user_account do |ch_f| = f.text_field :username - if @account_form.errors.messages[:username].present? - @account_form.errors.messages[:username].each do |m| = m = ch_f.text_field :financial_institution_name - if @account_form.errors.messages[:"general_user_account.financial_institution_name"].present? - @account_form.errors.messages[:"general_user_account.financial_institution_name"].each do |m| = m ...
ちなみに 「実践Ruby-Rails-4-現場のプロから学ぶ本格Webプログラミング」 のコードでは Virtus のようなものは使わずに form_for のしたに fields_for を使って実装してた
つまり同じことを上記の例ですると
= form_for @account_form, url: {action: 'update'} do |f| = f.fields_for :general_user_account, f.object.general_user do |ff| = ff.fields_for :general_user_account, ff.object.general_user_account do |fff| = ff.text_field :username .. = fff.text_field :financial_institution_name ..
みたいな感じになります。