deferred_associations 0.5.5 → 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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