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

Instance Method Details

#add_deletion_callbackObject



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