Search code examples
ruby-on-rails-3associationshas-manybelongs-to

Rails: How do I transactionally add a has_many association to an existing model?


Let's imagine I run an imaginary art store with a couple models (and by models I'm referring to the Rails term not the arts term as in nude models) that looks something like this:

class Artwork < ActiveRecord::Base
  belongs_to :purchase
  belongs_to :artist
end

class Purchase < ActiveRecord::Base
  has_many :artworks
  belongs_to :customer
end

The Artwork is created and sometime later it is included in a Purchase. In my create or update controller method for Purchase I would like to associate the new Purchase with the existing Artwork.

If the Artwork did not exist I could do @purchase.artworks.build or @purchase.artworks.create, but these both assume that I'm creating a new Artwork which I am not. I could add the existing artwork with something like this:

params[:artwork_ids].each do |artwork|
  @purchase.artworks << Artwork.find(artwork)
end

However, this isn't transactional. The database is updated immediately. (Unless of course I'm in the create controller in which case I think it may be done "transactionally" since the @purchase doesn't exist until I call save, but that doesn't help me for update.) There is also the @purchase.artwork_ids= method, but that is immediate as well.

I think something like this will work for the update action, but it is very inelegant.

@purchase = Purchase.find(params[:id])
result = @purchase.transaction do
  @purchase.update_attributes(params[:purchase])
  params[:artwork_ids].each do |artwork|
    artwork.purchase = @purchase
    artwork.save!
  end
end

This would be followed by the conventional:

if result 
  redirect_to purchase_url(@purchase), notice: 'Purchase was successfully updated.' }
else
  render action: "edit"
end

What I'm looking for is something like the way it would work from the other direction where I could just put accepts_nested_attributes_for in my model and then call result = @artwork.save and everything works like magic.


Solution

  • I have figured out a way to do what I want which fairly elegant. I needed to make updates to each part of my Product MVC.

    Model:

    attr_accessible: artwork_ids
    

    I had to add artwork_ids to attr_accessible since it wasn't included before.

    View:

    = check_box_tag "purchase[artwork_ids][]", artwork.id, artwork.purchase == @purchase
    

    In my view I have an array for each artwork with a check_box_tag. I couldn't use check_box because of the gotcha where not checking the box would cause a hidden value of "true" to be sent instead of an artwork id. However, this leaves me with the problem of deleting all the artwork from a purchase. When doing update, if I uncheck each check box, then the params[:purchase] hash won't have an :artwork_ids entry.

    Controller:

    params[:purchase][:artwork_ids] ||= []
    

    Adding this guarantees that the value is set, and will have the desired effect of removing all existing associations. However, this causes a pesky rspec failure Purchase.any_instance.should_receive(:update_attributes).with({'these' => 'params'}) fails because :update_attributes actually received {"these"=>"params", "artwork_ids"=>[]}). I tried setting a hidden_value_tag in the view instead, but couldn't get it to work. I think this nit is worthy of a new question.