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