#ruby
#Rails5
#form
#nested_attributes
#associations

Railsフォームのnested_attributesを使って、デフォルト値の異なる複数の関連レコードを作成する方法

記事を書くきっかけ

インターン先でタイトルのような実装をするに当たって、当初やり方がわからずググってもわかりやすい記事がなく四苦八苦した経験からです。日本語の記事はなく、結局方法はstackoverflowの投稿から見つけました。

Railsのformで、nested_attributesを使って複数の関連レコードを自動的に生成する実装を先日しました。
一番苦労した点は、デフォルト値の設定です。複数の関連レコードにそれぞれ同じデフォルト値を設定するのは比較的容易なのに、別々のものを設定するとなるとなかなか方法がわかりませんでした(汗)
なので、記録がてら記事を投稿しようと思います。

環境

Ruby 2.3.3
Rails 5.0.6

モデルの関係

モデル
class Group < ApplicationRecord
  has_many :users, through: :group_users
  has_many :group_users
end

class User < ApplicationRecord
  has_many :groups, through: :group_users
  has_many :group_users
end

class GroupUser < ApplicationRecord
  belongs_to :group, optional: true #nested_attributesの関係
  belongs_to :user
end

例として、GroupとUserがGroupUser(中間テーブル)を通じて結びついてる構造を使います。
mixiのコミュニティーとユーザーの関係と同じような構造ですかね?笑

GroupUserでbelongs_to :group, optional: trueが付いている理由は、Rails5からbelongs_toで自動的にバリデーションがかかるようになったからです。
つまり、group_idに値がないUserはバリデーションに引っかかって作れません!
ここでnested_attributesを使っているから起きる問題があります。

そしてRailsでバリデーションと保存の順番は大まかに

  1. Parentのバリデーション
  2. Childのバリデーション
  3. Parentの保存処理
  4. Childの保存処理 の順で行われ、Parentの保存がされて生まれたIDがChildのparent_idに入ります。

引用元:Rails5でnested attributesに詰まった話
http://www.te-nu.com/entry/2016/07/05/223000

よって、nested_attributesを使う時にはバリデーションに引っかからないためにoptional: trueが必要となります。Rails4以前は必要ではありません。

コントローラー

今回はgroups_controllerのアクションでGroupを作成し、関連するGroupUserを2つ自動で生成します。
つまりグループを作ると同時に、グループに属するユーザー2名を決めちゃいます。

app/controllers/groups_controller.rb
class GamesController < ApplicationController

  def new
    @group = Group.new
    2.times { @group.group_users.build } # @groupに関連するGroupUserを2回build
  end

  def create
    @group = Group.create(group_params)
    redirect_to @group
  end

  private

  def group_params
    params.require(:group).permit(:name, users_attributes: [:user_id])
  end

end

通常のビュー

まず、デフォルト値なしの場合はこうなります。
簡単のため、GroupUserを作成する入力欄にはUserのidを入力する仕様にします(現実にはありえないですね笑笑)

app/views/games/_form.html.erb
<%= form_for @group do |f| %>

  <!-- Group名 -->
  <%= f.label :name %>
  <%= f.text_field :name %>

  <!-- GroupUserを作成 -->
  <%= f.fields_for :group_users do |g| %>
    <%= g.label :user_id %>
    <%= g.number_field :user_id %>
  <% end %>

<% end %>

ここで、groupsコントローラーで2.times { @group.group_users.build }をしていたので、fields_forは1回書くだけでブラウザには2回表示されます。

別々のデフォルト値のあるビュー

例えば、groupの一人目はgroupの作成者が入る可能性が非常に高いので、その人の値を先にセットしちゃいたい場合はどうするのでしょうか?
この時、Rails4.0.2で追加されたnested_attributesのindex機能が超絶スーパー活躍します。

app/views/games/_form.html.erb
<%= form_for @group do |f| %>

  <!-- Group名 -->
  <%= f.label :name %>
  <%= f.text_field :name %>

  <!-- GroupUserを作成 -->
  <%= f.fields_for :group_users do |g| %>
    <%= g.label :user_id %>
    <% if g.index == 0 %>
      <%= g.number_field :user_id, value: current_user.id %>
    <% else %>
      <%= g.number_field :user_id %>
    <% end %>
  <% end %>

<% end %>

2回buildされたgroup_userのうち、1個目(index=0)はcurrent_user.idがデフォルト値として入り、2個目(index=1)はデフォルト値なしで表示されます。
index、便利〜〜〜!

参考

Rails: fields_for with index?
https://stackoverflow.com/questions/4853373/rails-fields-for-with-index

Rails5でnested attributesに詰まった話
http://www.te-nu.com/entry/2016/07/05/223000