Category : Ruby

※ 20140307 一部追記修正しました
new/create時に関連テーブルも更新する記述について、忘れていたのでまとめてみました。
(意外と参考書とかにも載っておらず・・・)
今回利用しているバージョンは下記。

  • Ruby -> 2.1.0
  • rails -> 4.0.2

やりたいこと

20140225_rails_view

こんなイメージです。postを1件更新するときに関連テーブル(authors)と中間テーブル(author_posts)を複数件数一気に更新する感じです。

Model

  • いつものhas_many through
  • posts <-> author_posts <-> authors
  • authorsは “name” カラムを持つ
  • formでネストさせるので、accepts_nested_attributes_for も設定します
# app/models/post.rb
class Post < ActiveRecord::Base
	has_many :author_posts
	has_many :authors, :through => :author_posts
	
	accepts_nested_attributes_for :authors
end
# app/models/author.rb
class Author < ActiveRecord::Base
	has_many :author_posts
	has_many :posts, :through => :author_posts
end
# app/models/author_post.rb
class AuthorPost < ActiveRecord::Base
	belongs_to :post
	belongs_to :author
end

まずはirbで動作を確認

# postのインスタンスを作成
irb > post = Post.new
=> #<Post id: nil, title: nil, created_at: nil, updated_at: nil>

# 空のインスタンスを確認
irb > post
=> #<Post id: nil, title: nil, created_at: nil, updated_at: nil>

# post.author_postsの確認
# 空のArray
irb > post.author_posts
=> #<ActiveRecord::Associations::CollectionProxy []>

# post.authorsの確認
# 空のArray
irb > post.authors
=> #<ActiveRecord::Associations::CollectionProxy []>

# buildで、postの関連テーブルauthorのインスタンスを作成
irb > post.authors.build
=> #<Author id: nil, name: nil, created_at: nil, updated_at: nil>

# post.authorsのArrayに、作成したインスタンスが格納されている
irb > post.authors
=> #<ActiveRecord::Associations::CollectionProxy [#<Author id: nil, name: nil, created_at: nil, updated_at: nil>]>

# 試しにもう一回buildしてみる
irb > post.authors.build
=> #<Author id: nil, name: nil, created_at: nil, updated_at: nil>

# post.authorsのArrayに、2個目のインスタンスがpushされている
irb > post.author
=> #<ActiveRecord::Associations::CollectionProxy [#<Author id: nil, name: nil, created_at: nil, updated_at: nil>, #<Author id: nil, name: nil, created_at: nil, updated_at: nil>]>

# post.author_posts のArrayはこの時点では空
irb > post.author_posts
=> #<ActiveRecord::Associations::CollectionProxy []>

# saveしてみる
# 関連テーブルにもINSERTされている
irb > post.save
   (0.5ms)  BEGIN
  SQL (4.4ms)  INSERT INTO `posts` (`created_at`, `updated_at`) VALUES ('2014-02-23 08:20:58', '2014-02-23 08:20:58')
  SQL (0.2ms)  INSERT INTO `authors` (`created_at`, `updated_at`) VALUES ('2014-02-23 08:20:58', '2014-02-23 08:20:58')
  SQL (0.3ms)  INSERT INTO `author_posts` (`author_id`, `created_at`, `post_id`, `updated_at`) VALUES (3, '2014-02-23 08:20:58', 10, '2014-02-23 08:20:58')
  SQL (0.2ms)  INSERT INTO `authors` (`created_at`, `updated_at`) VALUES ('2014-02-23 08:20:58', '2014-02-23 08:20:58')
  SQL (0.2ms)  INSERT INTO `author_posts` (`author_id`, `created_at`, `post_id`, `updated_at`) VALUES (4, '2014-02-23 08:20:58', 10, '2014-02-23 08:20:58')
   (2.2ms)  COMMIT
=> true

# save後に post.author_posts を再確認
# Arrayに値が入っている
irb > post.author_posts
=> #<ActiveRecord::Associations::CollectionProxy [#<Author id: 1, name: nil, created_at: "2014-02-23 08:41:41", updated_at: "2014-02-23 08:41:41">, #<Author id: 2, name: nil, created_at: "2014-02-23 08:41:41", updated_at: "2014-02-23 08:41:41">]>

関連オブジェクトの値を更新する際は、post.authors.build のようにする必要があります。
複数個の場合は複数回buildしてあげればよさそうです。
上記を元に、View、Controllerを作っていきます。

View(フォーム)

# app/views/posts/_form.html.erb
<%= form_for(@post) do |f| %>

	<% @post.authors.each do |author| %>
		<%= f.fields_for :authors, author do |author_field| %>
			<%= author_field.text_field :name %>
		<% end %>
	<% end %>

<% end %>


“authors_attributes[]”の部分は、普通は:authorsと書くようですが・・・
複数個更新するときには上記のように書くと、同名のパラメータを配列に格納してくれます。
また、authorsのnameカラムを更新したいので、text_fieldには :name と記述します。

追記):authors でも動作しました。

ポストされるパラメータ
params[:post][:authors_attributes]の中身は下記になります。

"authors_attributes"=>[{"name"=>"著者1"}, {"name"=>"著者2"}, {"name"=>"著者3"}]}

(実験)f.fields_forの引数を :authors にした場合のパラメータ

"authors_attributes"=>{"0"=>{"name"=>"あ"}, "1"=>{"name"=>"い"}, "2"=>{"name"=>"う"}}}


keyに個数を表す数値が入ってきてしまいました。
これが気持ち悪くて配列で取得するようにしています。

追記)ハッシュ形式の方が正しいようです。

Controller

# app/controllers/posts_controller.rb
def new
	@post = Post.new

	# 今回は分かりやすく、authorは固定で3枠作成
	# 1postで最大3author追加出来る

	3.times {
		# 関連オブジェクトをbuild
		@post.authors.build
	}
end

def create
	@post = Post.new(post_params);
	@post.save
end

# strong parameters
private
def post_params
	params.require(:post).permit(:title, authors_attributes: [:name])
end


本当はcreateに何も追記したくなかったのですが・・・(これ以外の方法がわかりませんでした。)
また、strong parametersのホワイトリストに :authors_attributes => [] を追記しています。

追記)上記のようにstrong parametersに記述をすることで、余計な処理を書かずに更新出来ました。
ただし、データを取り出して何らかの加工をする場合には、Arrayで取得しても良さそうです。

参考