module ActiveRecord::Associations::ClassMethods
Public Instance Methods
add_deletion_callback()
click to toggle source
# File lib/has_and_belongs_to_many_with_deferred_save.rb, line 120 def add_deletion_callback # this will delete all the association into the join table after obj.destroy, # but is only useful/necessary, if the record is not paranoid? unless respond_to?(:paranoid?) && paranoid? after_destroy do |record| begin record.save rescue Exception => e logger.warn "Association cleanup after destroy failed with #{e}" end end end end
define_id_getter(collection_name, collection_singular_ids)
click to toggle source
# File lib/has_many_with_deferred_save.rb, line 76 def define_id_getter(collection_name, collection_singular_ids) define_method "#{collection_singular_ids}_with_deferred_save" do send(collection_name).map { |e| e[:id] } end alias_method(:"#{collection_singular_ids}_without_deferred_save", :"#{collection_singular_ids}") alias_method(:"#{collection_singular_ids}", :"#{collection_singular_ids}_with_deferred_save") end
define_id_setter(collection_name, collection_singular_ids)
click to toggle source
# File lib/has_many_with_deferred_save.rb, line 62 def define_id_setter(collection_name, collection_singular_ids) # only needed for ActiveRecord >= 3.0 if ActiveRecord::VERSION::STRING >= '3' define_method "#{collection_singular_ids}_with_deferred_save=" do |ids| ids = Array.wrap(ids).reject(&:blank?) new_values = send("#{collection_name}_without_deferred_save").klass.find(ids) send("#{collection_name}=", new_values) end alias_method(:"#{collection_singular_ids}_without_deferred_save=", :"#{collection_singular_ids}=") alias_method(:"#{collection_singular_ids}=", :"#{collection_singular_ids}_with_deferred_save=") end end
define_obj_getter(collection_name)
click to toggle source
# File lib/has_many_with_deferred_save.rb, line 37 def define_obj_getter(collection_name) define_method("#{collection_name}_with_deferred_save") do save_in_progress = instance_variable_get "@hmwds_#{collection_name}_save_in_progress" # while updating the association, rails loads the association object - this needs to be the original one unless save_in_progress elements = instance_variable_get "@hmwds_temp_#{collection_name}" if elements.nil? elements = ArrayToAssociationWrapper.new(send("#{collection_name}_without_deferred_save")) elements.defer_association_methods_to self, collection_name instance_variable_set "@hmwds_temp_#{collection_name}", elements end result = elements else result = send("#{collection_name}_without_deferred_save") end result end alias_method(:"#{collection_name}_without_deferred_save", :"#{collection_name}") alias_method(:"#{collection_name}", :"#{collection_name}_with_deferred_save") end
define_obj_setter(collection_name)
click to toggle source
# File lib/has_many_with_deferred_save.rb, line 27 def define_obj_setter(collection_name) define_method("#{collection_name}_with_deferred_save=") do |objs| instance_variable_set "@hmwds_temp_#{collection_name}", objs || [] attribute_will_change!(collection_name) if objs != send("#{collection_name}_without_deferred_save") end alias_method(:"#{collection_name}_without_deferred_save=", :"#{collection_name}=") alias_method(:"#{collection_name}=", :"#{collection_name}_with_deferred_save=") end
define_reload_method(collection_name)
click to toggle source
# File lib/has_many_with_deferred_save.rb, line 100 def define_reload_method(collection_name) define_method "reload_with_deferred_save_for_#{collection_name}" do |*args| # Reload from the *database*, discarding any unsaved changes. send("reload_without_deferred_save_for_#{collection_name}", *args).tap do instance_variable_set "@hmwds_temp_#{collection_name}", nil end end alias_method(:"reload_without_deferred_save_for_#{collection_name}", :reload) alias_method(:reload, :"reload_with_deferred_save_for_#{collection_name}") end
define_update_method(collection_name)
click to toggle source
# File lib/has_many_with_deferred_save.rb, line 84 def define_update_method(collection_name) define_method "hmwds_update_#{collection_name}" do unless frozen? elements = instance_variable_get "@hmwds_temp_#{collection_name}" unless elements.nil? # nothing has been done with the association # save is done automatically, if original behaviour is restored instance_variable_set "@hmwds_#{collection_name}_save_in_progress", true send("#{collection_name}_without_deferred_save=", elements) instance_variable_set "@hmwds_#{collection_name}_save_in_progress", false instance_variable_set "@hmwds_temp_#{collection_name}", nil end end end end
has_and_belongs_to_many_with_deferred_save(*args)
click to toggle source
Instructions:
Replace your existing call to has_and_belongs_to_many with has_and_belongs_to_many_with_deferred_save.
Then add a validation method that adds an error if there is something wrong with the (unsaved) collection. This will prevent it from being saved if there are any errors.
Example:
def validate if people.size > maximum_occupancy errors.add :people, "There are too many people in this room" end end
# File lib/has_and_belongs_to_many_with_deferred_save.rb, line 17 def has_and_belongs_to_many_with_deferred_save(*args) collection_name = args[0].to_s collection_singular_ids = collection_name.singularize + '_ids' return if method_defined?("#{collection_name}_with_deferred_save") has_and_belongs_to_many *args add_deletion_callback attr_accessor :"unsaved_#{collection_name}" attr_accessor :"use_original_collection_reader_behavior_for_#{collection_name}" define_method "#{collection_name}_with_deferred_save=" do |collection| # puts "has_and_belongs_to_many_with_deferred_save: #{collection_name} = #{collection.collect(&:id).join(',')}" send "unsaved_#{collection_name}=", collection end define_method "#{collection_name}_with_deferred_save" do |*method_args| if send("use_original_collection_reader_behavior_for_#{collection_name}") send("#{collection_name}_without_deferred_save") else send("initialize_unsaved_#{collection_name}", *method_args) if send("unsaved_#{collection_name}").nil? send("unsaved_#{collection_name}") end end alias_method(:"#{collection_name}_without_deferred_save=", :"#{collection_name}=") alias_method(:"#{collection_name}=", :"#{collection_name}_with_deferred_save=") alias_method(:"#{collection_name}_without_deferred_save", :"#{collection_name}") alias_method(:"#{collection_name}", :"#{collection_name}_with_deferred_save") define_method "#{collection_singular_ids}_with_deferred_save" do |*method_args| if send("use_original_collection_reader_behavior_for_#{collection_name}") send("#{collection_singular_ids}_without_deferred_save") else send("initialize_unsaved_#{collection_name}", *method_args) if send("unsaved_#{collection_name}").nil? send("unsaved_#{collection_name}").map { |e| e[:id] } end end alias_method(:"#{collection_singular_ids}_without_deferred_save", :"#{collection_singular_ids}") alias_method(:"#{collection_singular_ids}", :"#{collection_singular_ids}_with_deferred_save") # only needed for ActiveRecord >= 3.0 if ActiveRecord::VERSION::STRING >= '3' define_method "#{collection_singular_ids}_with_deferred_save=" do |ids| ids = Array.wrap(ids).reject(&:blank?) reflection_wrapper = send("#{collection_name}_without_deferred_save") new_values = reflection_wrapper.klass.find(ids) send("#{collection_name}=", new_values) end alias_method(:"#{collection_singular_ids}_without_deferred_save=", :"#{collection_singular_ids}=") alias_method(:"#{collection_singular_ids}=", :"#{collection_singular_ids}_with_deferred_save=") end define_method "do_#{collection_name}_save!" do # Question: Why do we need this @use_original_collection_reader_behavior stuff? # Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only # records that have changed. # In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not # knowing that we've changed its behavior. It expects that method to return the elements of that collection that are in the *database* # (the original behavior), so we have to provide that behavior... If we didn't provide it, it would end up trying to take the diff of # two identical collections so nothing would ever get saved. # But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use # @use_original_collection_reader_behavior as a switch. unless send("unsaved_#{collection_name}").nil? send "use_original_collection_reader_behavior_for_#{collection_name}=", true # vv This is where the actual save occurs vv send "#{collection_name}_without_deferred_save=", send("unsaved_#{collection_name}") send "use_original_collection_reader_behavior_for_#{collection_name}=", false end true end after_save :"do_#{collection_name}_save!" define_method "reload_with_deferred_save_for_#{collection_name}" do |*method_args| # Reload from the *database*, discarding any unsaved changes. send("reload_without_deferred_save_for_#{collection_name}", *method_args).tap do send "unsaved_#{collection_name}=", nil # /\ If we didn't do this, then when we called reload, it would still have the same (possibly invalid) value of # unsaved_collection that it had before the reload. end end alias_method(:"reload_without_deferred_save_for_#{collection_name}", :reload) alias_method(:reload, :"reload_with_deferred_save_for_#{collection_name}") define_method "initialize_unsaved_#{collection_name}" do |*method_args| elements = send("#{collection_name}_without_deferred_save", *method_args) # here the association will be duped, so changes to "unsaved_#{collection_name}" will not be saved immediately elements = ArrayToAssociationWrapper.new(elements) elements.defer_association_methods_to self, collection_name send "unsaved_#{collection_name}=", elements end private :"initialize_unsaved_#{collection_name}" end
has_many_with_deferred_save(*args)
click to toggle source
# File lib/has_many_with_deferred_save.rb, line 4 def has_many_with_deferred_save(*args) collection_name = args[0].to_s collection_singular_ids = "#{collection_name.singularize}_ids" return if method_defined?("#{collection_name}_with_deferred_save") has_many *args if args[1].is_a?(Hash) && args[1].keys.include?(:through) logger.warn "You are using the option :through on #{name}##{collection_name}. This was not tested very much with has_many_with_deferred_save. Please write many tests for your functionality!" end after_save :"hmwds_update_#{collection_name}" define_obj_setter collection_name define_obj_getter collection_name define_id_setter collection_name, collection_singular_ids define_id_getter collection_name, collection_singular_ids define_update_method collection_name define_reload_method collection_name end