has_manyな関連先をまとめてINSERTする
fields_for、accepts_nested_attributes_forを使って、has_manyな関連先をまとめてINSERTする方法。ソースはgithubに上げておいた。ちなみにRails 3.0.8。
Post has_many Tags through Taggingsというモデルがあったとする。
とりあえずscaffoldはこんな感じ。Taggingだけは画面が要らないのでmodelだけ。
$ rails g scaffold posts title:string text:text $ rails g scaffold tags name:string $ rails g model tagging post:references tag:references $ rake db:migrate
コードの修正で重要なのは次の2点。
app/models/post.rb に accepts_nested_attributes_forを設定する。
class Post < ActiveRecord::Base has_many :taggings has_many :tags, :through => :taggings accepts_nested_attributes_for :taggings end
これで次のようなコードが実行された時にTaggingもまとめて作ってくれるようになる。
Post.create( :title => "タイトル", :text => "本文", :taggings_attributes => [ { :tag_id => 1 }, { :tag_id => 2 } ] )
app/views/posts/_form.html に fields_for を使ってリレーション先についてのフォームを作る。第一引数がtaggings_attributes[]になっているのがポイント。これはaccepts_nested_attributes_forに合わせて設定する。
<div class="field"> Tags<br /> <% @taggings.each do |tagging| %> <%= f.fields_for "taggings_attributes[]", tagging do |tf| %> <%= tf.select :tag_id, Tag.all.map { |x| [x.name, x.id] } %> <% end %> <% end %> </div>
なお、 @taggings はapp/controllers/posts_controller.rbで事前に用意しておく。 @postからリレーションで辿らないのは、新規にPostを作るフォームでは@post.taggingsが必ず空なのでeachが回らないから。
# GET /posts/new # GET /posts/new.xml def new @post = Post.new @taggings = Array.new(3) { Tagging.new } respond_to do |format| format.html # new.html.erb format.xml { render :xml => @post } end end # GET /posts/1/edit def edit @post = Post.find(params[:id]) @taggings = @post.taggings end
あとはサーバを起動して/tagsからタグを登録し、/postsから記事を投稿すればok。 一応どんな感じの画面になるのかスクリーンショットを載せておこう。
作り込むなら、
- accepts_nested_attributes_for のreject_ifオプションでtag_idがblankなら無視する
- dependentにdestroyを設定する
- Taggingでpost_idとtag_idのペアが一致するものが複数できないように制限をかける
などの点が気になるけど、今回は置いておく。
accepts_nested_attributes_for自体はhas_oneでも使えて、それに合わせてfields_forの引数も単数形にすれば良い。詳しくはリファレンスを参照。
追記
fields_forのなかでidも指定してればUPDATEも走らせてくれる。
<div class="field"> Tags<br /> <% @taggings.each do |tagging| %> <%= f.fields_for "taggings_attributes[]", tagging do |tf| %> <% if tagging.persisted? %> <%= tf.hidden_field :id, value: tagging.id %> <% end %> <%= tf.select :tag_id, Tag.all.map { |x| [x.name, x.id] } %> <% end %> <% end %> </div>
あと、Rails3.2.3以降では親のモデルにattr_accessibleを指定する必要がある。
class Post < ActiveRecord::Base has_many :taggings has_many :tags, :through => :taggings accepts_nested_attributes_for :taggings attr_accessible :taggings_attributes end
ついでに、fields_for
で末尾に付けた[]
をdate_select
、datetime_select
ヘルパメソッドは削除してしまうっぽい(ソースは追ってない)。prefix
オプションをちゃんと指定してあげる必要がある。
tf.datetime_select :created_at, prefix: "post[taggings][]"