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

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

Railsでaccepts_nested_attributes_forとfields_forを使ってhas_many関連の子レコードを作成/更新するフォームを作成

Rails中級 Rails Controller Rails View

Railsでは、accepts_nested_attributes_forを使うことで簡単に1対多のモデルを一度に更新するフォームを作成することできます。

今回のケースは、ユーザー(User)が家、会社など複数の住所(Address)を持っているというという1対多関係のモデルの入れ子状態を説明します。
完成イメージは次の通りで、ユーザー情報(User)も作成しながら、住所(Address)も一緒に1フォームで作成できるようになっています。
f:id:nipe880324:20141127225732p:plain:w420


動作確認

  • Rails 4.1

目次

  1. Railsプロジェクトの作成
  2. accepts_nested_attributes_forによる入れ子のフォームを作成
  3. 入れ子にしたモデルの削除機能の追加
  4. 親モデルと子モデルにバリデーションを追加する
  5. Ajaxで動的に住所の入力項目を追加/削除する

1. Railsプロジェクトの作成

まずプロジェクトを作成します。

rails new accepts_nested_attributes_for_test
cd accepts_nested_attributes_for_test


ユーザーをScaffoldで作成する。
コントローラー、ビュー、モデル、マイグレーションファイルといったユーザーの一式が作成されます。

rails g scaffold User username age:integer


住所モデルを作成する。
今回は、1つのフォームでユーザーと住所の作成をするのでモデルとマイグレーションファイルのみ作成します。

rails g model Address zipcode city street tel user_id:integer


マイグレートを実行する。

rake db:migrate

アソシエーションを追加する。ユーザーと住所は、1対N関係なので次のようにします。

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :addresses
end


# app/models/address.rb
class Address < ActiveRecord::Base
  belongs_to :user
end


2. accepts_nested_attributes_forによる入れ子のフォームを作成

ActiveRecordは入れ子のフォームを取扱うためにaccepts_nested_attributes_forメソッドを提供しているので、それを追加します。
これを追加することで、addresses_attributes=が自動的に追加され、それにより、住所(Address)の追加、更新、削除ができるようになります。

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :addresses
  accepts_nested_attributes_for :addresses
end


注意事項として、次のようにpresence: trueのバリデーションを定義すると保存ができなくなるので注意してください。

# app/models/address.rb
class Address < ActiveRecord::Base
  belongs_to :user
  # モデルを保存できなくなるのでpresence: trueは記載しない
  validates :user_id, presence: true
  # or validates :user, presence: true
end


では、フォームで値を入力/更新できるようにします。
fields_forを使うことで入れ子のモデル(Address)を表示させることができます。

# app/views/users/_form.html.erb

   ...
  <div class="field">
    <%= f.label :age %><br>
    <%= f.number_field :age %>
  </div>
  <div class="field">
    <%= f.label :addresses, "住所" %><br />
    <%= f.fields_for :addresses do |addresses_form| %>
      <%= addresses_form.label :zipcode, "郵便番号" %>
      <%= addresses_form.text_field :zipcode %><br />
      <%= addresses_form.label :city, "都道府県" %>
      <%= addresses_form.text_field :city %><br />
      <%= addresses_form.label :street, "市町村番" %>
      <%= addresses_form.text_field :street %><br />
      <%= addresses_form.label :tel, "電話番号" %>
      <%= addresses_form.text_field :tel %><br />
    <% end %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>


次に、コントローラーでnewアクションでAddressをbuild(newのリレーション版)をするようにします。こうすることで、fields_for内の内容が表示されるようになります。
また、StrongParameterも許可するようにします。fields_for[引数に与えた名前]_attributesというname属性でフォームを作成するので、addresses_attributesを許可する必要があります。

# app/controllers/users_controller.rb

  # GET /users/new
  def new
    @user = User.new
    @user.addresses.build

    # デフォルトで2つの住所入力欄を作成したい場合は次のようにする
    # 2.times { @user.addresses.build }
  end

  ...

  private

    def user_params
      params.require(:user).permit(
        :username,
        :age,
        addresses_attributes: [:id, :zipcode, :city, :street, :tel]
      )
    end


では、動作確認してみましょう。
入力画面で次のように値をいれて新規作成します。
f:id:nipe880324:20141127220131p:plain:w380


すると、accepts_nested_attributes_forfields_forにより、自動的にユーザーと住所モデルが作成されます。
そして、編集画面を開くと登録されていることがわかると思います。
f:id:nipe880324:20141127220314p:plain:w380


登録時のサーバーのログを確認すると次のようなパラメーターとSQL文が走っています。

# パラメーター(addresses_attributesにより入力パラメーターが渡されている)
 {"utf8"=>"", "authenticity_token"=>"3t7C...",
  "user"=> {
    "username"=>"test1",
    "age"=>"10",
    "addresses_attributes" => {
      "0" => {
        "zipcode" => "111-1111",
        "city" => "東京都",
        "street" => "", 
        "tel"=>""
       }
     }
   },
  "commit"=>"Create User"
 }

# SQL文(UserとAddressモデルが作成されている) 
  (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "users" ("age", "created_at", "updated_at", "username") VALUES (?, ?, ?, ?)  [["age", 10], ["created_at", "2014-11-27 12:56:56.445149"], ["updated_at", "2014-11-27 12:56:56.445149"], ["username", "test1"]]
  SQL (0.3ms)  INSERT INTO "addresses" ("city", "created_at", "street", "tel", "updated_at", "user_id", "zipcode") VALUES (?, ?, ?, ?, ?, ?, ?)  [["city", "東京都"], ["created_at", "2014-11-27 12:56:56.450941"], ["street", ""], ["tel", ""], ["updated_at", "2014-11-27 12:56:56.450941"], ["user_id", 1], ["zipcode", "111-1111"]]


3. 入れ子にしたモデルの削除機能の追加

accepts_nested_attributes_forを使っていれば、簡単にユーザーに関連した住所モデルを削除することもできます。
削除方法として、フォームに削除チェックボックスを追加し、チェックしたまま更新するとそのモデルを削除されるようにします。


まず、allow_destroy: trueオプションをユーザーモデルに追加します。

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :addresses
  accepts_nested_attributes_for :addresses, allow_destroy: true
end


次に、削除チェックボックスをフォームに追加します。
_destroyキーである必要があります。
accepts_nested_attributes_forの挙動として、_destroy1trueの場合に、関連するモデルが削除されるような挙動になっているためです。

# app/views/users/_form.html.erb
  ...
  <div class="field">
    <%= f.field_for :addresses do |addresses_form| %>
      ...

      <%= addresses_form.label :street, "市町村番" %>
      <%= addresses_form.text_field :street %><br />
      <%= addresses_form.label :tel, "電話番号" %>
      <%= addresses_form.text_field :tel %><br />
      <!-- DBに保存されていない場合のみ表示 = 更新時のみ表示 -->
      <% if @user.persisted? %>
        <%= addresses_form.check_box :_destroy %>
        <%= addresses_form.label :_destroy, "削除" %><br />
      <% end %>
    <% end %>
  </div>
  ...


最後に、StrongParameterに_destroyをpermitするようにします。

# app/controllers/users_controller.rb
 
  private

    def user_params
      params.require(:user).permit(
        :username,
        :age,
        addresses_attributes: [:id, :zipcode, :city, :street, :tel, :_destroy]
      )
    end


では、削除できることを確認しましょう。
編集画面を開き、「削除」にチェックし、更新ボタンを押します。
f:id:nipe880324:20141127221518p:plain:w380


編集画面を開きなおすと、住所が削除されます。Railsにより自動的にレコードが削除されました。
f:id:nipe880324:20141127221524p:plain:w380




4. 親モデルと子モデルにバリデーションを追加する

reject_ifで条件を指定して値を更新しないようにすることができますが、画面上にエラーとして表示されないため、バリデーションを定義します。
定義方法は通常通りです。

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :addresses
  accepts_nested_attributes_for :addresses, allow_destroy: true

  # presence: true 各項目が存在しない場合バリデーションエラーになる
  validates :username,  presence: true
  validates :age,       presence: true
end


# app/models/address.rb
class Address < ActiveRecord::Base
  belongs_to :user

  # presence: true 各項目が存在しない場合バリデーションエラーになる
  validates :zipcode,  presence: true
  validates :city,     presence: true
  validates :street,   presence: true
  validates :tel,      presence: true
end


では、画面から未入力でユーザーを新規登録すると、バリデーションが適用されていることがわかります。
このように、accepts_nested_attributes_forfields_forメソッドによりバリデーションも自動的に行ってくれます。
f:id:nipe880324:20141127223728p:plain:w420


ちなみに、今は、住所を入力する気がなくてもバリデーションが実行されてしまいユーザーが登録できません。
そういうときにreject_ifオプションが使えます。これがtrueになるときは住所モデルを作成せず、バリデーションも実施しません。
条件の指定方法は次のようにProcかall_blankのどちらかで記述します。

# 郵便番号が空の場合のみ、住所モデルを作成しない(バリデーションも実行しない)
# つまり、郵便番号を入力した場合は、バリデーションが実行されます。
accepts_nested_attributes_for :addresses, allow_destroy: true, reject_if: proc { |attributes| attributes['zipcode'].blank? }

# すべての入力属性が空の場合、住所モデルを作成しない(バリデーションも実行しない)
# 1つでも属性を入力した場合、住所モデルを作成しようとします。(バリデーションも実行される)
accepts_nested_attributes_for :addresses, allow_destroy: true, reject_if: :all_blank


では、Userモデルを次のように修正します。

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :addresses
  accepts_nested_attributes_for :addresses, allow_destroy: true, reject_if: :all_blank

  # presence: true 各項目が存在しない場合バリデーションエラーになる
  validates :username,  presence: true
  validates :age,       presence: true
end


では、画面を開いて未入力で作成ボタンを押すと、Userのバリデーションエラーのみ発生します。
これは、reject_if: :all_blankがtrueになったため住所モデルを作成しなくなったため、バリデーションエラーがUserのみになったためです。
f:id:nipe880324:20141209202629p:plain:w420




5. Ajaxで動的に住所の入力項目を追加/削除する

「追加」、「削除」リンクを押すことで、動的に入力項目を追加/削除するため機能は提供していないので、Ajaxで作る必要があります。


まずは、jQueryとturbolinkを共存させるために、jquery-turbolinksをインストールします。

echo "gem 'jquery-turbolinks'" >> Gemfile
bundle install


jquery.turbolinksも読み込むようにします。

# app/assets/javascripts/application.js
...
//= require jquery
//= require jquery.turbolinks
//= require jquery_ujs


住所入力部分を動的に追加、削除するので、住所入力部分を部分テンプレートをします。
ついでに、削除はリンクにし、JSで動的に非表示にするようにします。

# app/views/users/_address_fields.html.erb
<fieldset>
  <%= f.label :zipcode, "郵便番号" %>
  <%= f.text_field :zipcode %><br />
  <%= f.label :city, "都道府県" %>
  <%= f.text_field :city %><br />
  <%= f.label :street, "市町村番" %>
  <%= f.text_field :street %><br />
  <%= f.label :tel, "電話番号" %>
  <%= f.text_field :tel %><br />
  <%= f.hidden_field :_destroy %>
  <%= link_to "削除", '#', class: "remove_fields" %>
</fieldset>


次に、作成した部分テンプレートを呼び出すようにし、「住所追加」リンクも作ります。

# app/views/users/_form.html.erb
  <div class="field">
    <%= f.label :addresses, "住所" %><br />
    <%= f.fields_for :addresses do |builder| %>
      <%= render "address_fields", f: builder %>
    <% end %>
    <%= link_to_add_fields "住所追加", f, :addresses %>
  </div>


次に、link_to_add_fieldsヘルパーを実装します。

# app/helpers/application_helper.rb
module ApplicationHelper
  def link_to_add_fields(name, f, association)
    new_object = f.object.send(association).klass.new
    id = new_object.object_id
    fields = f.fields_for(association, new_object, child_index: id) do |builder|
      render(association.to_s.singularize + "_fields", f: builder)
    end
    link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
  end
end


そして、最後はAjax部分をCoffeeScriptで記載します。

# app/assets/javascripts/user.js.coffee
$ ->
  $('form').on 'click', '.remove_fields', (event) ->
    $(this).prev('input[type=hidden]').val('1')
    $(this).closest('fieldset').hide()
    event.preventDefault()

  $('form').on 'click', '.add_fields', (event) ->
    time = new Date().getTime()
    regexp = new RegExp($(this).data('id'), 'g')
    $(this).before($(this).data('fields').replace(regexp, time))
    event.preventDefault()


では、動作を確認してみましょう。
「住所追加」や「削除」ができ、また、作成や更新をすると値が更新されます。
f:id:nipe880324:20141127225732p:plain:w420


以上です。