RoRでDBテーブルのカラムと一致しない項目がある入力フォームを実現する その6
さて、これまで幾つかの方法を記しておいたが、どれもアドホック、その場凌ぎの辛うじて小規模な開発で許されるような拙い方法であることは否めない。もっとRoRに相応しい洗練された美しい方法がある。その方法を用いなければ、例えば親子関係があるテーブルの子テーブルの情報を編集するのに、ビューでその子の個数を増減させたり新規作成・編集してからDBに保存するといった処理をするのに態々少し複雑なコードを書かなくてはならなくなる。今回はその方法を書き残しておく。
元にする具体例
accepts_nested_attributes_for とfields_for を使った動的に子情報のためのフォームを増減させられる例を示したページは幾つかあるが、今回は次のサイトを参考にした:
ruby-rails.hatenadiary.com
上記サイトではUser(親)とAddress(子)という関係のモデルをつかっているが、この備忘録ではCustomer(親)とShipping(子)とした。顧客(customer)の発送先(shpipping)が複数あるかもしれないから発送先の住所(address)入力してもらおうという感じで、Amazonなどを思い出すと分かり易いだろう。モデルCustomerが複数のモデルShppingを全くもたないか、一つ以上持ち、Shppingにはadddressという属性があるとする。
環境やバージョンによっては、双方向関連付けを「明示」しておいてその関係を通知させるようにしておかないと、エラーがおきたり思った通りに動かなかったりすることがあるだろう。
scaffoldから作れば10分もかからずにできてしまうだろう。
実現方法
カラムと一致しない項目がある入力フォームにする
カラムと一致しない項目がある入力フォームにしないと始まらない。
前出の具体例を参考に、子フォームの増減やCRUD処理が動くことが確認できたら、この備忘録ではshippingのaddressを、以前の備忘録のように、フォームでは直接入力できず、state(都道府県)、city(市区町村)、street(町名字番地等)に分けて入力することにし、格納するときはその三つの項目を半角項目を挟んで一つの文字列にすることにする。具体的には、shipping用のフォームとして差し込まれるファイル内容を書き換える:
<fieldset> <%#= f.label :address %> <%#= f.text_field :address %><br /> <%= f.label :state %> <%= f.text_field :state %><br /> <%= f.label :city %> <%= f.text_field :city %><br /> <%= f.label :street %> <%= f.text_field :street %><br /> <%= f.hidden_field :_destroy %> <%= link_to "削除", '#', class: "remove_fields" %> </fieldset>
いつものことだが、書き換える前のコードをコメントアウトして残している。
コントローラー
Railsのモデルの仕様を理解すればコントローラーの変更箇所は少なくて済む。今回もそうなった。なんとStrongParameterを実行する箇所だけ変更すればよい。
private # 省略 # Never trust parameters from the scary internet, only allow the white list through. def customer_params params.require(:customer).permit( :name, #:shippings_attributes => [:id, :address, :_destroy] :shippings_attributes => [:id, :state, :city, :street, :_destroy] ) end
adrressをstate、city、streetの三つに書き換えている。また、idがあるおかげで複数のshippingを扱え、_destroyがあるおかげで個別に削除が可能となっている。
モデル
以前まではコントローラー中にストロングパラメーターを無事に通過して得たハッシュから新規作成または更新すべき情報を作りだしていたが、そういった処理全てモデル内でしかもカプセル化することが可能である。様々なタイミングで実行することができるコールバック(フック)が何種類も用意してあり、それを利用して実現する。
新規作成時
Railsのコールバックのタイミングを詳しく記したページに譲るが、DBに新規に書き込む書き込む前に、バリデーションがあればバリーデーションを実行するが、before_validationでメソッドなどを指定しておけばバリデーション前にその処理を実行することが可能である。
この備忘録の場合、その処理でstate、city、streetからaddressの文字列を合成すればよい。ソースコードは更新時と一緒に示す。
更新時
こちらも同様に、コールバックを利用するが、コントローラーのedit時に実行されるfindメソッドの実行直後、そしてコントローラーのupdate時に実行されるfindメソッドの実行直後とモデルのupdate時の動作を考慮して次のコードにすればよい:
class Shipping < ApplicationRecord before_validation :make_address after_find :split_address belongs_to :customer validates :address, presence: true attr_accessor :state, :city, :street validates :state, presence: true validates :city, presence: true validates :street, presence: true private def make_address # logger.debug(">>>>> make_address") # logger.debug(self.changes) self.address = self.state.to_s + ' ' + self.city.to_s + ' ' + self.street.to_s # logger.debug(@self.changes) end def split_address # logger.debug(">>>>> split_address") # logger.debug(self.changes) self.state, self.city, self.street = self.address.to_s.split(' ') self.address = '' # logger.debug(self.changes) end end
コメントアウトしているロガーを機能させれば動作がよくわかる。更新のとき、先ずコントローラーのeditの処理が走る。そしてフォームでボタンが押されると、コントローラーのupdate処理が走る。editとupdate、そのどちらでもモデルのfindメソッドによって該当するレコードの情報をモデルに移しているのは既に分かっていることだろう。
editの処理においては、コールバックafter_findで呼び出された上記のsplit_addressを実行すると、addressにあった情報が、フォーム用のstate、city、streetに移され、adressはblankにされる。さて、フォームでsubmitされた後、再度、addressからstate、city、streetに情報が分割されて格納され、そのあとでaddressがblankにさられる。そしてモデルのupdateメソッドがコントローラーで実行されると、直ぐにbefore_validationで登録された処理make_adressが走る。ここで、state、city、streetに変更がなかった場合、addressはDBからモデルに移した時と同じ値を持つので変更の必要がなく、更新処理は終了し、DBの更新も生じない。その三つのうちどれか変更があれば、その三つからaddressに合成された情報と、DBからモデルに移した時のaddressが異なるので変更があったとみなされ、DBへの更新処理が始まる。
参考まとめ
accepts_nested_attributes_for:
ruby-rails.hatenadiary.com
turbolinks:
railsguides.jp
inverse_of:
railsguides.jp
callbacks:
techracho.bpsinc.jp
qiita.com
changes:
www.techscore.com