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.
- checksums.yaml +15 -0
- data/CHANGELOG +5 -0
- data/Rakefile +27 -27
- data/Readme.markdown +84 -84
- data/deferred_associations.gemspec +61 -61
- data/init.rb +1 -1
- data/lib/has_and_belongs_to_many_with_deferred_save.rb +142 -142
- data/lib/has_many_with_deferred_save.rb +3 -3
- data/spec/db/database.yml +21 -21
- data/spec/db/schema.rb +40 -40
- data/spec/has_and_belongs_to_many_with_deferred_save_spec.rb +244 -236
- data/spec/models/door.rb +3 -3
- data/spec/models/person.rb +15 -15
- data/spec/models/room.rb +51 -51
- data/spec/spec_helper.rb +46 -46
- metadata +3 -9
@@ -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 :
|
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 :
|
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
|