読者です 読者をやめる 読者になる 読者になる

Matthewの備忘録

忘れたときはここを見ろ。何か書いてある。

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

 実際に更新と作成のフォームが振り分けられているかどうかは実行してブラウザでソースをみればよい。

 尚、この方法はRuby 2.2.6または2.4以上、Rails 5.0以上で確認した。

追記

 この方法を使う場合、form_forに渡されるオブジェクト/インスタンスに新規か更新かを判断するための属性が全く含まれていないので、:urlの:actionを指定しないとエラーとなります。この方法を使う場合は:urlの:actionを指定します。

*1:フォームが新規作成用で生成され、更新ボタンを押してもコントローラーのcreateを実行しようとし、ルーティングによってはエラーダンプ表示となる。

*2:2017/05/16日時点

*3:respond_to?で調べることができる。