isra17
isra17

Reputation: 411

Updating association without saving it

I have a model:

class A < ActiveRecord::Base
  has_many :B
end

And I want to reset or update A's B association, but only save it later:

a = A.find(...)
# a.bs == [B<...>, B<...>]
a.bs = [] 
#or
a.bs = [B.new, B.new]
# do some validation stuff on `a` and `a.bs`

So there might be some case where I will call a.save later or maybe not. In the case I don't call a.save I would like that a.bs stay to its original value, but as soon as I call a.bs = [], the old associations is destroyed and now A.find(...).bs == []. Is there any simple way to set a record association without persisting it in the database right away? I looked at Rails source and didn't find anything that could help me there.

Thanks!

Edit:

I should add that this is for an existing application and there are some architecture constraint that doesn't allow us to use the the regular ActiveRecord updating and validation tools. The way it works we have a set of Updater class that take params and assign the checkout object the value from params. There are then a set of Validater class that validate the checkout object for each given params. Fianlly, if everything is good, we save the model.

In this case, I'm looking to update the association in an Updater, validate them in the Validator and finally, persist it if everything check out.

In summary, this would look like:

def update
    apply_updaters(object, params)
    # do some stuff with the updated object
    if(validate(object))
        object.save(validate: false)
end

Since there are a lot of stuff going on between appy_updaters and object.save, Transaction are not really an option. This is why I'm really looking to update the association without persisting right away, just like we would do with any other attribute.

So far, the closest solution I've got to is rewriting the association cache (target). This look something like:

# In the updater
A.bs.target.clear
params[:bs].each{|b| A.bs.build(b)}
# A.bs now contains the parameters object without doing any update in the database

When come the time to save, we need to persist cache:

new_object = A.bs.target
A.bs(true).replace(new_object)

This work, but this feel kind of hack-ish and can easily break or have some undesired side-effect. An alternative I'm thinking about is to add a method A#new_bs= that cache the assigned object and A#bs that return the cached object if available.

Upvotes: 3

Views: 2721

Answers (2)

isra17
isra17

Reputation: 411

I finally found out about the mark_for_destruction method. My final solution therefor look like:

a.bs.each(&:mark_for_destruction)
params[:bs].each{|b| a.bs.build(b)}

And then I can filter out the marked_for_destruction? entry in the following processing and validation.

Thanks @AlkH that made me look into how accepts_nested_attributes_for was working and handling delayed destruction of association.

Upvotes: 2

AlkH
AlkH

Reputation: 321

Good question. I can advice to use attributes assignment instead of collection manipulation. All validations will be performed as regular - after save or another 'persistent' method. You can write your own method (in model or in separated validator) which will validate collection.

You can delete and add elements to collection through attributes - deletion is performed by additional attribute _destroy which may be 'true' or 'false' (http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html), addition - through setting up parent model to accept attributes.

As example set up model A:

class A < ActiveRecord::Base
   has_many :b
   accepts_nested_attributes_for :b, :allow_destroy => true
   validates_associated :b # to validate each element

   validate :b_is_correct # to validate whole collection
   def b_is_correct
      self.bs.each { |b| ... } # validate collection
   end
end

In controller use plain attributes for model updating (e.g update!(a_aparams)). These methods will behave like flat attribute updating. And don't forget to permit attributes for nested collection.

class AController < ApplicationController
  def update
    @a = A.find(...)
    @a.update(a_attributes) # triggers validation, if error occurs - no changes will be persisted and a.errors will be populated
  end

  def a_attributes
    params.require(:a).permit([:attr_of_a, :b_attributes => [:attr_of_b, :_destroy]])
  end
end

On form we used gem nested_form (https://github.com/ryanb/nested_form), I recommend it. But on server side this approach uses attribute _destroy as mentioned before.

Upvotes: 2

Related Questions