Alessio Caiazza is sharing code with you
Bitbucket is a code hosting site. Unlimited public and private repositories. Free for small teams.
Don't show this againhabtm-with-deferred-save / has_and_belongs_to_many_with_deferred_save / lib / has_and_belongs_to_many_with_deferred_save.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | # To do: make it work to call this twice in a class. Currently that probably wouldn't work, because it would try to alias methods to existing names...
# Note: before_save must be defined *before* including this module, not after.
module ActiveRecord
module Associations
module ClassMethods
# 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
def has_and_belongs_to_many_with_deferred_save(*args)
has_and_belongs_to_many *args
collection_name = args[0].to_s
collection_singular_ids = collection_name.singularize + "_ids"
# this will delete all the assocation into the join table after obj.destroy
after_destroy { |record| record.save }
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(',')}"
self.send "unsaved_#{collection_name}=", collection
end
define_method "#{collection_name}_with_deferred_save" do |*args|
if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
self.send("#{collection_name}_without_deferred_save")
else
if self.send("unsaved_#{collection_name}").nil?
send("initialize_unsaved_#{collection_name}", *args)
end
self.send("unsaved_#{collection_name}")
end
end
alias_method_chain :"#{collection_name}=", 'deferred_save'
alias_method_chain :"#{collection_name}", 'deferred_save'
define_method "#{collection_singular_ids}_with_deferred_save" do |*args|
if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
self.send("#{collection_singular_ids}_without_deferred_save")
else
if self.send("unsaved_#{collection_name}").nil?
send("initialize_unsaved_#{collection_name}", *args)
end
self.send("unsaved_#{collection_name}").map { |e| e[:id] }
end
end
alias_method_chain :"#{collection_singular_ids}", 'deferred_save'
define_method "before_save_with_deferred_save_for_#{collection_name}" 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.
if self.respond_to? :"before_save_without_deferred_save_for_#{collection_name}"
self.send("before_save_without_deferred_save_for_#{collection_name}")
end
self.send "use_original_collection_reader_behavior_for_#{collection_name}=", true
if self.send("unsaved_#{collection_name}").nil?
send("initialize_unsaved_#{collection_name}", *args)
end
self.send "#{collection_name}_without_deferred_save=", self.send("unsaved_#{collection_name}")
# /\ This is where the actual save occurs.
self.send "use_original_collection_reader_behavior_for_#{collection_name}=", false
true
end
alias_method_chain :"before_save", "deferred_save_for_#{collection_name}"
define_method "reload_with_deferred_save_for_#{collection_name}" do
# Reload from the *database*, discarding any unsaved changes.
returning self.send("reload_without_deferred_save_for_#{collection_name}") do
self.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_chain :"reload", "deferred_save_for_#{collection_name}"
define_method "initialize_unsaved_#{collection_name}" do |*args|
#puts "Initialized to #{self.send("#{collection_name}_without_deferred_save").clone.inspect}"
self.send "unsaved_#{collection_name}=", self.send("#{collection_name}_without_deferred_save", *args).clone
# /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the
# database, in which case we want unsaved_collection to start out with the "saved collection".
# If they just constructed a *new* object, this will still work, because self.collection_without_deferred_save.clone
# will return a new HasAndBelongsToManyAssociation (which acts like an empty array, []).
# Important: If we don't use clone, then it does an assignment by reference and any changes to unsaved_collection
# will also change *collection_without_deferred_save*! (Not what we want! Would result in us saving things
# immediately, which is exactly what we're trying to avoid.)
# trick collection_name.include?(obj)
# If you use a collection of SignelTableInheritance and didn't :select 'type' the
# include? method will not find any subclassed object.
class << eval("@unsaved_#{collection_name}")
def include_with_deferred_save?(obj)
if self.find { |itm| itm == obj || (itm[:id] == obj[:id] && obj.is_a?(itm.class) ) }
return true
else
return false
end
end
alias_method_chain :include?, 'deferred_save'
end
end
private :"initialize_unsaved_#{collection_name}"
end
end
end
end
|