Module: ActiveRecord::Associations::ClassMethods
- Defined in:
- lib/has_many_with_deferred_save.rb,
lib/has_and_belongs_to_many_with_deferred_save.rb
Instance Method Summary collapse
- #add_deletion_callback ⇒ Object
- #define_id_getter(collection_name, collection_singular_ids) ⇒ Object
- #define_id_setter(collection_name, collection_singular_ids) ⇒ Object
- #define_obj_getter(collection_name) ⇒ Object
- #define_obj_setter(collection_name) ⇒ Object
- #define_reload_method(collection_name) ⇒ Object
- #define_update_method(collection_name) ⇒ Object
-
#has_and_belongs_to_many_with_deferred_save(*args) ⇒ Object
Instructions:.
- #has_many_with_deferred_save(*args) ⇒ Object
Instance Method Details
#add_deletion_callback ⇒ Object
120 121 122 123 124 125 126 127 128 129 130 131 132 |
# 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) ⇒ Object
76 77 78 79 80 81 82 |
# 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) ⇒ Object
62 63 64 65 66 67 68 69 70 71 72 73 74 |
# 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) ⇒ Object
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# 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) ⇒ Object
27 28 29 30 31 32 33 34 35 |
# 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) ⇒ Object
100 101 102 103 104 105 106 107 108 109 |
# 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) ⇒ Object
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# 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) ⇒ Object
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
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# 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) ⇒ Object
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# 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 |