deferred_associations 0.5.5 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,142 +1,142 @@
1
- module ActiveRecord
2
- module Associations
3
- module ClassMethods
4
-
5
- # Instructions:
6
- #
7
- # Replace your existing call to has_and_belongs_to_many with has_and_belongs_to_many_with_deferred_save.
8
- #
9
- # 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.
10
- #
11
- # Example:
12
- #
13
- # def validate
14
- # if people.size > maximum_occupancy
15
- # errors.add :people, "There are too many people in this room"
16
- # end
17
- # end
18
- def has_and_belongs_to_many_with_deferred_save(*args)
19
- has_and_belongs_to_many *args
20
- collection_name = args[0].to_s
21
- collection_singular_ids = collection_name.singularize + "_ids"
22
-
23
- add_deletion_callback
24
-
25
- attr_accessor :"unsaved_#{collection_name}"
26
- attr_accessor :"use_original_collection_reader_behavior_for_#{collection_name}"
27
-
28
- define_method "#{collection_name}_with_deferred_save=" do |collection|
29
- #puts "has_and_belongs_to_many_with_deferred_save: #{collection_name} = #{collection.collect(&:id).join(',')}"
30
- self.send "unsaved_#{collection_name}=", collection
31
- end
32
-
33
- define_method "#{collection_name}_with_deferred_save" do |*args|
34
- if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
35
- self.send("#{collection_name}_without_deferred_save")
36
- else
37
- if self.send("unsaved_#{collection_name}").nil?
38
- send("initialize_unsaved_#{collection_name}", *args)
39
- end
40
- self.send("unsaved_#{collection_name}")
41
- end
42
- end
43
-
44
- alias_method_chain :"#{collection_name}=", 'deferred_save'
45
- alias_method_chain :"#{collection_name}", 'deferred_save'
46
-
47
- define_method "#{collection_singular_ids}_with_deferred_save" do |*args|
48
- if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
49
- self.send("#{collection_singular_ids}_without_deferred_save")
50
- else
51
- if self.send("unsaved_#{collection_name}").nil?
52
- send("initialize_unsaved_#{collection_name}", *args)
53
- end
54
- self.send("unsaved_#{collection_name}").map { |e| e[:id] }
55
- end
56
- end
57
-
58
- alias_method_chain :"#{collection_singular_ids}", 'deferred_save'
59
-
60
- # only needed for ActiveRecord >= 3.0
61
- if ActiveRecord::VERSION::STRING >= "3"
62
- define_method "#{collection_singular_ids}_with_deferred_save=" do |ids|
63
- ids = Array.wrap(ids).reject { |id| id.blank? }
64
- reflection_wrapper = self.send("#{collection_name}_without_deferred_save")
65
- new_values = reflection_wrapper.klass.find(ids)
66
- self.send("#{collection_name}=", new_values)
67
- end
68
- alias_method_chain :"#{collection_singular_ids}=", 'deferred_save'
69
- end
70
-
71
- define_method "do_#{collection_name}_save!" do
72
- # Question: Why do we need this @use_original_collection_reader_behavior stuff?
73
- # Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only
74
- # records that have changed.
75
- # In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not
76
- # knowing that we've changed its behavior. It expects that method to return the elements of that collection that are in the *database*
77
- # (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
78
- # two identical collections so nothing would ever get saved.
79
- # But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use
80
- # @use_original_collection_reader_behavior as a switch.
81
-
82
- self.send "use_original_collection_reader_behavior_for_#{collection_name}=", true
83
- if self.send("unsaved_#{collection_name}").nil?
84
- send("initialize_unsaved_#{collection_name}")
85
- end
86
- self.send "#{collection_name}_without_deferred_save=", self.send("unsaved_#{collection_name}")
87
- # /\ This is where the actual save occurs.
88
- self.send "use_original_collection_reader_behavior_for_#{collection_name}=", false
89
-
90
- true
91
- end
92
- after_save "do_#{collection_name}_save!"
93
-
94
-
95
- define_method "reload_with_deferred_save_for_#{collection_name}" do
96
- # Reload from the *database*, discarding any unsaved changes.
97
- self.send("reload_without_deferred_save_for_#{collection_name}").tap do
98
- self.send "unsaved_#{collection_name}=", nil
99
- # /\ If we didn't do this, then when we called reload, it would still have the same (possibly invalid) value of
100
- # unsaved_collection that it had before the reload.
101
- end
102
- end
103
- alias_method_chain :"reload", "deferred_save_for_#{collection_name}"
104
-
105
-
106
- define_method "initialize_unsaved_#{collection_name}" do |*args|
107
- #puts "Initialized to #{self.send("#{collection_name}_without_deferred_save").clone.inspect}"
108
- elements = self.send("#{collection_name}_without_deferred_save", *args).clone
109
- elements = ArrayToAssociationWrapper.new(elements)
110
- elements.defer_association_methods_to self, collection_name
111
- self.send "unsaved_#{collection_name}=", elements
112
- # /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the
113
- # database, in which case we want unsaved_collection to start out with the "saved collection".
114
- # Actually, this doesn't clone the Association but the elements array instead (since the clone method is
115
- # proxied like any other methods)
116
- # Important: If we don't use clone, then it does an assignment by reference and any changes to unsaved_collection
117
- # will also change *collection_without_deferred_save*! (Not what we want! Would result in us saving things
118
- # immediately, which is exactly what we're trying to avoid.)
119
-
120
-
121
-
122
- end
123
- private :"initialize_unsaved_#{collection_name}"
124
-
125
- end
126
-
127
- def add_deletion_callback
128
- # this will delete all the association into the join table after obj.destroy,
129
- # but is only useful/necessary, if the record is not paranoid?
130
- unless (self.respond_to?(:paranoid?) && self.paranoid?)
131
- after_destroy { |record|
132
- begin
133
- record.save
134
- rescue Exception => e
135
- logger.warn "Association cleanup after destroy failed with #{e}"
136
- end
137
- }
138
- end
139
- end
140
- end
141
- end
142
- end
1
+ module ActiveRecord
2
+ module Associations
3
+ module ClassMethods
4
+
5
+ # Instructions:
6
+ #
7
+ # Replace your existing call to has_and_belongs_to_many with has_and_belongs_to_many_with_deferred_save.
8
+ #
9
+ # 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.
10
+ #
11
+ # Example:
12
+ #
13
+ # def validate
14
+ # if people.size > maximum_occupancy
15
+ # errors.add :people, "There are too many people in this room"
16
+ # end
17
+ # end
18
+ def has_and_belongs_to_many_with_deferred_save(*args)
19
+ has_and_belongs_to_many *args
20
+ collection_name = args[0].to_s
21
+ collection_singular_ids = collection_name.singularize + "_ids"
22
+
23
+ add_deletion_callback
24
+
25
+ attr_accessor :"unsaved_#{collection_name}"
26
+ attr_accessor :"use_original_collection_reader_behavior_for_#{collection_name}"
27
+
28
+ define_method "#{collection_name}_with_deferred_save=" do |collection|
29
+ #puts "has_and_belongs_to_many_with_deferred_save: #{collection_name} = #{collection.collect(&:id).join(',')}"
30
+ self.send "unsaved_#{collection_name}=", collection
31
+ end
32
+
33
+ define_method "#{collection_name}_with_deferred_save" do |*args|
34
+ if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
35
+ self.send("#{collection_name}_without_deferred_save")
36
+ else
37
+ if self.send("unsaved_#{collection_name}").nil?
38
+ send("initialize_unsaved_#{collection_name}", *args)
39
+ end
40
+ self.send("unsaved_#{collection_name}")
41
+ end
42
+ end
43
+
44
+ alias_method_chain :"#{collection_name}=", 'deferred_save'
45
+ alias_method_chain :"#{collection_name}", 'deferred_save'
46
+
47
+ define_method "#{collection_singular_ids}_with_deferred_save" do |*args|
48
+ if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
49
+ self.send("#{collection_singular_ids}_without_deferred_save")
50
+ else
51
+ if self.send("unsaved_#{collection_name}").nil?
52
+ send("initialize_unsaved_#{collection_name}", *args)
53
+ end
54
+ self.send("unsaved_#{collection_name}").map { |e| e[:id] }
55
+ end
56
+ end
57
+
58
+ alias_method_chain :"#{collection_singular_ids}", 'deferred_save'
59
+
60
+ # only needed for ActiveRecord >= 3.0
61
+ if ActiveRecord::VERSION::STRING >= "3"
62
+ define_method "#{collection_singular_ids}_with_deferred_save=" do |ids|
63
+ ids = Array.wrap(ids).reject { |id| id.blank? }
64
+ reflection_wrapper = self.send("#{collection_name}_without_deferred_save")
65
+ new_values = reflection_wrapper.klass.find(ids)
66
+ self.send("#{collection_name}=", new_values)
67
+ end
68
+ alias_method_chain :"#{collection_singular_ids}=", 'deferred_save'
69
+ end
70
+
71
+ define_method "do_#{collection_name}_save!" do
72
+ # Question: Why do we need this @use_original_collection_reader_behavior stuff?
73
+ # Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only
74
+ # records that have changed.
75
+ # In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not
76
+ # knowing that we've changed its behavior. It expects that method to return the elements of that collection that are in the *database*
77
+ # (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
78
+ # two identical collections so nothing would ever get saved.
79
+ # But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use
80
+ # @use_original_collection_reader_behavior as a switch.
81
+
82
+ self.send "use_original_collection_reader_behavior_for_#{collection_name}=", true
83
+ if self.send("unsaved_#{collection_name}").nil?
84
+ send("initialize_unsaved_#{collection_name}")
85
+ end
86
+ self.send "#{collection_name}_without_deferred_save=", self.send("unsaved_#{collection_name}")
87
+ # /\ This is where the actual save occurs.
88
+ self.send "use_original_collection_reader_behavior_for_#{collection_name}=", false
89
+
90
+ true
91
+ end
92
+ after_save "do_#{collection_name}_save!"
93
+
94
+
95
+ define_method "reload_with_deferred_save_for_#{collection_name}" do |*args|
96
+ # Reload from the *database*, discarding any unsaved changes.
97
+ self.send("reload_without_deferred_save_for_#{collection_name}", *args).tap do
98
+ self.send "unsaved_#{collection_name}=", nil
99
+ # /\ If we didn't do this, then when we called reload, it would still have the same (possibly invalid) value of
100
+ # unsaved_collection that it had before the reload.
101
+ end
102
+ end
103
+ alias_method_chain :reload, "deferred_save_for_#{collection_name}"
104
+
105
+
106
+ define_method "initialize_unsaved_#{collection_name}" do |*args|
107
+ #puts "Initialized to #{self.send("#{collection_name}_without_deferred_save").clone.inspect}"
108
+ elements = self.send("#{collection_name}_without_deferred_save", *args).clone
109
+ elements = ArrayToAssociationWrapper.new(elements)
110
+ elements.defer_association_methods_to self, collection_name
111
+ self.send "unsaved_#{collection_name}=", elements
112
+ # /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the
113
+ # database, in which case we want unsaved_collection to start out with the "saved collection".
114
+ # Actually, this doesn't clone the Association but the elements array instead (since the clone method is
115
+ # proxied like any other methods)
116
+ # Important: If we don't use clone, then it does an assignment by reference and any changes to unsaved_collection
117
+ # will also change *collection_without_deferred_save*! (Not what we want! Would result in us saving things
118
+ # immediately, which is exactly what we're trying to avoid.)
119
+
120
+
121
+
122
+ end
123
+ private :"initialize_unsaved_#{collection_name}"
124
+
125
+ end
126
+
127
+ def add_deletion_callback
128
+ # this will delete all the association into the join table after obj.destroy,
129
+ # but is only useful/necessary, if the record is not paranoid?
130
+ unless (self.respond_to?(:paranoid?) && self.paranoid?)
131
+ after_destroy { |record|
132
+ begin
133
+ record.save
134
+ rescue Exception => e
135
+ logger.warn "Association cleanup after destroy failed with #{e}"
136
+ end
137
+ }
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -98,13 +98,13 @@ module ActiveRecord
98
98
  end
99
99
 
100
100
  def define_reload_method collection_name
101
- define_method "reload_with_deferred_save_for_#{collection_name}" do
101
+ define_method "reload_with_deferred_save_for_#{collection_name}" do |*args|
102
102
  # Reload from the *database*, discarding any unsaved changes.
103
- self.send("reload_without_deferred_save_for_#{collection_name}").tap do
103
+ self.send("reload_without_deferred_save_for_#{collection_name}", *args).tap do
104
104
  instance_variable_set "@hmwds_temp_#{collection_name}", nil
105
105
  end
106
106
  end
107
- alias_method_chain :"reload", "deferred_save_for_#{collection_name}"
107
+ alias_method_chain :reload, "deferred_save_for_#{collection_name}"
108
108
  end
109
109
  end
110
110
  end
data/spec/db/database.yml CHANGED
@@ -1,21 +1,21 @@
1
- sqlite3:
2
- adapter: sqlite3
3
- database: test.sqlite3.db
4
-
5
- sqlite3mem:
6
- adapter: sqlite3
7
- database: ":memory:"
8
-
9
- postgresql:
10
- adapter: postgresql
11
- username: postgres
12
- password: postgres
13
- database: has_and_belongs_to_many_with_deferred_save_test
14
- min_messages: ERROR
15
-
16
- mysql:
17
- adapter: mysql
18
- host: localhost
19
- username: root
20
- password:
21
- database: has_and_belongs_to_many_with_deferred_save_test
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: test.sqlite3.db
4
+
5
+ sqlite3mem:
6
+ adapter: sqlite3
7
+ database: ":memory:"
8
+
9
+ postgresql:
10
+ adapter: postgresql
11
+ username: postgres
12
+ password: postgres
13
+ database: has_and_belongs_to_many_with_deferred_save_test
14
+ min_messages: ERROR
15
+
16
+ mysql:
17
+ adapter: mysql
18
+ host: localhost
19
+ username: root
20
+ password:
21
+ database: has_and_belongs_to_many_with_deferred_save_test
data/spec/db/schema.rb CHANGED
@@ -1,40 +1,40 @@
1
- # This file is autogenerated. Instead of editing this file, please use the
2
- # migrations feature of ActiveRecord to incrementally modify your database, and
3
- # then regenerate this schema definition.
4
-
5
- ActiveRecord::Schema.define(:version => 1) do
6
-
7
- create_table "people", :force => true do |t|
8
- t.column "name", :string
9
- end
10
-
11
- create_table "people_rooms", :id => false, :force => true do |t|
12
- t.column "person_id", :integer
13
- t.column "room_id", :integer
14
- end
15
-
16
- create_table "rooms", :force => true do |t|
17
- t.column "name", :string
18
- t.column "maximum_occupancy", :integer
19
- end
20
-
21
- create_table "doors_rooms", :id => false, :force => true do |t|
22
- t.column "door_id", :integer
23
- t.column "room_id", :integer
24
- end
25
-
26
- create_table "doors", :force => true do |t|
27
- t.column "name", :string
28
- end
29
-
30
- create_table "tables", :force => true do |t|
31
- t.column "name", :string
32
- t.column "room_id", :integer
33
- end
34
-
35
- create_table "chairs", :force => true do |t|
36
- t.column "name", :string
37
- t.column "table_id", :integer
38
- end
39
-
40
- end
1
+ # This file is autogenerated. Instead of editing this file, please use the
2
+ # migrations feature of ActiveRecord to incrementally modify your database, and
3
+ # then regenerate this schema definition.
4
+
5
+ ActiveRecord::Schema.define(:version => 1) do
6
+
7
+ create_table "people", :force => true do |t|
8
+ t.column "name", :string
9
+ end
10
+
11
+ create_table "people_rooms", :id => false, :force => true do |t|
12
+ t.column "person_id", :integer
13
+ t.column "room_id", :integer
14
+ end
15
+
16
+ create_table "rooms", :force => true do |t|
17
+ t.column "name", :string
18
+ t.column "maximum_occupancy", :integer
19
+ end
20
+
21
+ create_table "doors_rooms", :id => false, :force => true do |t|
22
+ t.column "door_id", :integer
23
+ t.column "room_id", :integer
24
+ end
25
+
26
+ create_table "doors", :force => true do |t|
27
+ t.column "name", :string
28
+ end
29
+
30
+ create_table "tables", :force => true do |t|
31
+ t.column "name", :string
32
+ t.column "room_id", :integer
33
+ end
34
+
35
+ create_table "chairs", :force => true do |t|
36
+ t.column "name", :string
37
+ t.column "table_id", :integer
38
+ end
39
+
40
+ end