記事を書くきっかけ
インターン先でタイトルのような実装をするに当たって、当初やり方がわからずググってもわかりやすい記事がなく四苦八苦した経験からです。日本語の記事はなく、結局方法は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でバリデーションと保存の順番は大まかに
- Parentのバリデーション
- Childのバリデーション
- Parentの保存処理
- 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名を決めちゃいます。
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を入力する仕様にします(現実にはありえないですね笑笑)
<%= 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機能が超絶スーパー活躍します。
<%= 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