RoRでDBテーブルのカラムと一致しない項目がある入力フォームを実現する その1
RoRでサイトを構築していると、DBのテーブルに存在しないか、テーブルにカラムがない項目を入力して何かさせたくなることがある。検索機能はその一つであろう。その方法は既に沢山の方によって英語や日本で紹介されている。次はその一つ:
qiita.com
検索機能を実装するならこれでよいが、CRUDを実装するには情報が不足している。より詳細に言うなら、更新(Update)が思った通りにならない。おそらく更新できず、意図しないルーティングに悩まされることだろう。ソースレベルでいうと、フォームを更新のつもりで生成させたのに新規作成で生成してしまう。
実際例
例えば次のようなDBにテーブルが無いモデル有るモデル、入力フォーム、そしてコントローラーが書かれたファイル三点セットでは、Nameテーブルにレコードを新規作成することは可能だが、更新ができない*1:
モデル:
class Name < ApplicationRecord validates :family_name, presence: true validates :first_name, presence: true end class NameForm include ActiveModel::Model attr_accessor :name end
ビュー(都合上一つにまとめています):
# new.html.erb <h1>New Name</h1> <%= render 'form', name_form: @name_form, action: :create %> <%= link_to 'Back', names_path %> # update.html.erb <h1>Edit Name</h1> <%= render 'form', name_form: @name_form, action: :update %> <%= link_to 'Back', names_path %> # _form.html.erb <%= form_for name_form, :url{:action=>action} do |f| %> <p> <%= f.label :name, 'フルネーム(半角空白で分かち書き):' %> <%= f.text_field :name %> </p> <p> <%= f.submit '登録する' %> </p> <% end %>
コントローラー:
class NameController < ApplicationController def new @name_form = NameForm.new end def create family, first = params.require(:name_form)[:name].split(' ') if Name.create(family_name: family, first_name: first) then redirect_to name_url(params[:id]), notice: '新規作成!' else render :new end end def edit name = Name.find(params[:id]) @name_form = NameForm.new(name: name.family_name + ' ' + name.first_name) end def update family, first = params.require(:name_form)[:name].split(' ') name = Name.find(params[:id]) if name.update_attributes(:family_name=>family, :first_name=>first) then redirect_to name_url(params[:id]), notice: '更新!' else render :edit end end def destroy # 処理は省略 end end
永続化状態取得メソッドが必要
更新フォームを生成させるには、モデルにpersisted?という永続化フラグを返すメソッドが存在してその返す値がtrueでなければならない。これはRailsのソースプログラムを読んで判明した情報だ:
github.com
form_helperソースコードの462行*2に更新(edit)か作成(new)かを振り分ける条件が書いてあり、persisted?メソッドが存在していて*3なおかつ返す値がtrueであるときに更新とする処理が書かれている。
上記に挙げたMVC例では、フォーム用のモデルNameFormにpersisted?メソッドを加え、その値を保持するためのインスタンス変数を加えてやればよい。
モデル:
class Name < ApplicationRecord validates :family_name, presence: true validates :first_name, presence: true end class NameForm include ActiveModel::Model attr_accessor :name, :persisted # 変数名はなんでもよい def persisted? @persisted || false # 初期値は:newで実行させるためfalseに end end
さらに、呼び出すコントローラー側でフォーム用のモデルのpersistedにtrueを与えて更新(edit)に、falseを与えて新規作成(new)に振り分ける処理を加えることで、更新も可能となる。
コントローラー:
class NameController < ApplicationController def new @name_form = NameForm.new end def create family, first = params.require(:name_form)[:name].split(' ') if Name.create(family_name: family, first_name: first) then redirect_to name_url(params[:id]), notice: '新規作成!' else render :new end end def edit name = Name.find(params[:id]) @name_form = NameForm.new( name: name.family_name + ' ' + name.first_name, persisted: true ) end def update family, first = params.require(:name_form)[:name].split(' ') name = Name.find(params[:id]) if name.update_attributes(:family_name=>family, :first_name=>first) then redirect_to name_url(params[:id]), notice: '更新!' else render :edit end end def destroy # 処理は省略 end end
実際に更新と作成のフォームが振り分けられているかどうかは実行してブラウザでソースをみればよい。
追記
この方法を使う場合、form_forに渡されるオブジェクト/インスタンスに新規か更新かを判断するための属性が全く含まれていないので、:urlの:actionを指定しないとエラーとなります。この方法を使う場合は:urlの:actionを指定します。