2 # AutosaveAssociation is a module that takes care of automatically saving
3 # your associations when the parent is saved. In addition to saving, it
4 # also destroys any associations that were marked for destruction.
5 # (See mark_for_destruction and marked_for_destruction?)
7 # Saving of the parent, its associations, and the destruction of marked
8 # associations, all happen inside 1 transaction. This should never leave the
9 # database in an inconsistent state after, for instance, mass assigning
10 # attributes and saving them.
12 # If validations for any of the associations fail, their error messages will
13 # be applied to the parent.
15 # Note that it also means that associations marked for destruction won't
16 # be destroyed directly. They will however still be marked for destruction.
18 # === One-to-one Example
20 # Consider a Post model with one Author:
23 # has_one :author, :autosave => true
26 # Saving changes to the parent and its associated model can now be performed
27 # automatically _and_ atomically:
30 # post.title # => "The current global position of migrating ducks"
31 # post.author.name # => "alloy"
33 # post.title = "On the migration of ducks"
34 # post.author.name = "Eloy Duran"
38 # post.title # => "On the migration of ducks"
39 # post.author.name # => "Eloy Duran"
41 # Destroying an associated model, as part of the parent's save action, is as
42 # simple as marking it for destruction:
44 # post.author.mark_for_destruction
45 # post.author.marked_for_destruction? # => true
47 # Note that the model is _not_ yet removed from the database:
49 # Author.find_by_id(id).nil? # => false
52 # post.reload.author # => nil
54 # Now it _is_ removed from the database:
55 # Author.find_by_id(id).nil? # => true
57 # === One-to-many Example
59 # Consider a Post model with many Comments:
62 # has_many :comments, :autosave => true
65 # Saving changes to the parent and its associated model can now be performed
66 # automatically _and_ atomically:
69 # post.title # => "The current global position of migrating ducks"
70 # post.comments.first.body # => "Wow, awesome info thanks!"
71 # post.comments.last.body # => "Actually, your article should be named differently."
73 # post.title = "On the migration of ducks"
74 # post.comments.last.body = "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
78 # post.title # => "On the migration of ducks"
79 # post.comments.last.body # => "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
81 # Destroying one of the associated models members, as part of the parent's
82 # save action, is as simple as marking it for destruction:
84 # post.comments.last.mark_for_destruction
85 # post.comments.last.marked_for_destruction? # => true
86 # post.comments.length # => 2
88 # Note that the model is _not_ yet removed from the database:
89 # id = post.comments.last.id
90 # Comment.find_by_id(id).nil? # => false
93 # post.reload.comments.length # => 1
95 # Now it _is_ removed from the database:
96 # Comment.find_by_id(id).nil? # => true
100 # Validation is performed on the parent as usual, but also on all autosave
101 # enabled associations. If any of the associations fail validation, its
102 # error messages will be applied on the parents errors object and validation
103 # of the parent will fail.
105 # Consider a Post model with Author which validates the presence of its name
109 # has_one :author, :autosave => true
113 # validates_presence_of :name
116 # post = Post.find(1)
117 # post.author.name = ''
118 # post.save # => false
119 # post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author_name"=>["can't be blank"]}, @base=#<Post ...>>
121 # No validations will be performed on the associated models when validations
122 # are skipped for the parent:
124 # post = Post.find(1)
125 # post.author.name = ''
126 # post.save(false) # => true
127 module AutosaveAssociation
128 ASSOCIATION_TYPES
= %w
{ has_one belongs_to has_many has_and_belongs_to_many
}
130 def self.included(base
)
132 base
.extend(ClassMethods
)
133 alias_method_chain
:reload, :autosave_associations
135 ASSOCIATION_TYPES
.each
do |type
|
136 base
.send("valid_keys_for_#{type}_association") << :autosave
144 # def belongs_to(name, options = {})
146 # add_autosave_association_callbacks(reflect_on_association(name))
148 ASSOCIATION_TYPES
.each
do |type
|
150 def #{type}(name, options = {})
152 add_autosave_association_callbacks(reflect_on_association(name
))
157 # Adds a validate and save callback for the association as specified by
159 def add_autosave_association_callbacks(reflection
)
160 save_method
= "autosave_associated_records_for_#{reflection.name}"
161 validation_method
= "validate_associated_records_for_#{reflection.name}"
162 validate validation_method
164 case reflection
.macro
165 when :has_many, :has_and_belongs_to_many
166 before_save
:before_save_collection_association
168 define_method(save_method
) { save_collection_association(reflection
) }
169 # Doesn't use after_save as that would save associations added in after_create/after_update twice
170 after_create save_method
171 after_update save_method
173 define_method(validation_method
) { validate_collection_association(reflection
) }
175 case reflection
.macro
177 define_method(save_method
) { save_has_one_association(reflection
) }
178 after_save save_method
180 define_method(save_method
) { save_belongs_to_association(reflection
) }
181 before_save save_method
183 define_method(validation_method
) { validate_single_association(reflection
) }
188 # Reloads the attributes of the object as usual and removes a mark for destruction.
189 def reload_with_autosave_associations(options
= nil)
190 @marked_for_destruction = false
191 reload_without_autosave_associations(options
)
194 # Marks this record to be destroyed as part of the parents save transaction.
195 # This does _not_ actually destroy the record yet, rather it will be destroyed when <tt>parent.save</tt> is called.
197 # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
198 def mark_for_destruction
199 @marked_for_destruction = true
202 # Returns whether or not this record will be destroyed as part of the parents save transaction.
204 # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
205 def marked_for_destruction
?
206 @marked_for_destruction
211 # Returns the record for an association collection that should be validated
212 # or saved. If +autosave+ is +false+ only new records will be returned,
213 # unless the parent is/was a new record itself.
214 def associated_records_to_validate_or_save(association
, new_record
, autosave
)
217 elsif association
.loaded
?
218 autosave
? association
: association
.select
{ |record
| record
.new_record
? }
220 autosave
? association
.target
: association
.target
.select
{ |record
| record
.new_record
? }
224 # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
225 # turned on for the association specified by +reflection+.
226 def validate_single_association(reflection
)
227 if reflection
.options
[:validate] == true || reflection
.options
[:autosave] == true
228 if (association
= association_instance_get(reflection
.name
)) && !association
.target
.nil?
229 association_valid
?(reflection
, association
)
234 # Validate the associated records if <tt>:validate</tt> or
235 # <tt>:autosave</tt> is turned on for the association specified by
237 def validate_collection_association(reflection
)
238 if reflection
.options
[:validate] !
= false && association
= association_instance_get(reflection
.name
)
239 if records
= associated_records_to_validate_or_save(association
, new_record
?, reflection
.options
[:autosave])
240 records
.each
{ |record
| association_valid
?(reflection
, record
) }
245 # Returns whether or not the association is valid and applies any errors to
246 # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
247 # enabled records if they're marked_for_destruction?.
248 def association_valid
?(reflection
, association
)
249 unless valid
= association
.valid
?
250 if reflection
.options
[:autosave]
251 unless association
.marked_for_destruction
?
252 association
.errors
.each
do |attribute
, message
|
253 attribute
= "#{reflection.name}_#{attribute}"
254 errors
.add(attribute
, message
) unless errors
.on(attribute
)
258 errors
.add(reflection
.name
)
264 # Is used as a before_save callback to check while saving a collection
265 # association whether or not the parent was a new record before saving.
266 def before_save_collection_association
267 @new_record_before_save = new_record
?
271 # Saves any new associated records, or all loaded autosave associations if
272 # <tt>:autosave</tt> is enabled on the association.
274 # In addition, it destroys all children that were marked for destruction
275 # with mark_for_destruction.
277 # This all happens inside a transaction, _if_ the Transactions module is included into
278 # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
279 def save_collection_association(reflection
)
280 if association
= association_instance_get(reflection
.name
)
281 autosave
= reflection
.options
[:autosave]
283 if records
= associated_records_to_validate_or_save(association
, @new_record_before_save, autosave
)
284 records
.each
do |record
|
285 if autosave
&& record
.marked_for_destruction
?
286 association
.destroy(record
)
287 elsif @new_record_before_save || record
.new_record
?
289 association
.send(:insert_record, record
, false, false)
291 association
.send(:insert_record, record
)
299 # reconstruct the SQL queries now that we know the owner's id
300 association
.send(:construct_sql) if association
.respond_to
?(:construct_sql)
304 # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
305 # on the association.
307 # In addition, it will destroy the association if it was marked for
308 # destruction with mark_for_destruction.
310 # This all happens inside a transaction, _if_ the Transactions module is included into
311 # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
312 def save_has_one_association(reflection
)
313 if (association
= association_instance_get(reflection
.name
)) && !association
.target
.nil?
314 if reflection
.options
[:autosave] && association
.marked_for_destruction
?
316 elsif new_record
? || association
.new_record
? || association
[reflection
.primary_key_name
] !
= id
|| reflection
.options
[:autosave]
317 association
[reflection
.primary_key_name
] = id
318 association
.save(false)
323 # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
324 # on the association.
326 # In addition, it will destroy the association if it was marked for
327 # destruction with mark_for_destruction.
329 # This all happens inside a transaction, _if_ the Transactions module is included into
330 # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
331 def save_belongs_to_association(reflection
)
332 if association
= association_instance_get(reflection
.name
)
333 if reflection
.options
[:autosave] && association
.marked_for_destruction
?
336 association
.save(false) if association
.new_record
? || reflection
.options
[:autosave]
338 if association
.updated
?
339 self[reflection
.primary_key_name
] = association
.id
340 # TODO: Removing this code doesn't seem to matter…
341 if reflection
.options
[:polymorphic]
342 self[reflection
.options
[:foreign_type]] = association
.class.base_class
.name
.to_s