741aa2acbef2e38781cb6237da6f2a6418a680d8
[feedcatcher.git] / vendor / rails / activerecord / lib / active_record / autosave_association.rb
1 module ActiveRecord
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?)
6 #
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.
11 #
12 # If validations for any of the associations fail, their error messages will
13 # be applied to the parent.
14 #
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.
17 #
18 # === One-to-one Example
19 #
20 # Consider a Post model with one Author:
21 #
22 # class Post
23 # has_one :author, :autosave => true
24 # end
25 #
26 # Saving changes to the parent and its associated model can now be performed
27 # automatically _and_ atomically:
28 #
29 # post = Post.find(1)
30 # post.title # => "The current global position of migrating ducks"
31 # post.author.name # => "alloy"
32 #
33 # post.title = "On the migration of ducks"
34 # post.author.name = "Eloy Duran"
35 #
36 # post.save
37 # post.reload
38 # post.title # => "On the migration of ducks"
39 # post.author.name # => "Eloy Duran"
40 #
41 # Destroying an associated model, as part of the parent's save action, is as
42 # simple as marking it for destruction:
43 #
44 # post.author.mark_for_destruction
45 # post.author.marked_for_destruction? # => true
46 #
47 # Note that the model is _not_ yet removed from the database:
48 # id = post.author.id
49 # Author.find_by_id(id).nil? # => false
50 #
51 # post.save
52 # post.reload.author # => nil
53 #
54 # Now it _is_ removed from the database:
55 # Author.find_by_id(id).nil? # => true
56 #
57 # === One-to-many Example
58 #
59 # Consider a Post model with many Comments:
60 #
61 # class Post
62 # has_many :comments, :autosave => true
63 # end
64 #
65 # Saving changes to the parent and its associated model can now be performed
66 # automatically _and_ atomically:
67 #
68 # post = Post.find(1)
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."
72 #
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."
75 #
76 # post.save
77 # post.reload
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."
80 #
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:
83 #
84 # post.comments.last.mark_for_destruction
85 # post.comments.last.marked_for_destruction? # => true
86 # post.comments.length # => 2
87 #
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
91 #
92 # post.save
93 # post.reload.comments.length # => 1
94 #
95 # Now it _is_ removed from the database:
96 # Comment.find_by_id(id).nil? # => true
97 #
98 # === Validation
99 #
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.
104 #
105 # Consider a Post model with Author which validates the presence of its name
106 # attribute:
107 #
108 # class Post
109 # has_one :author, :autosave => true
110 # end
111 #
112 # class Author
113 # validates_presence_of :name
114 # end
115 #
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 ...>>
120 #
121 # No validations will be performed on the associated models when validations
122 # are skipped for the parent:
123 #
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 }
129
130 def self.included(base)
131 base.class_eval do
132 base.extend(ClassMethods)
133 alias_method_chain :reload, :autosave_associations
134
135 ASSOCIATION_TYPES.each do |type|
136 base.send("valid_keys_for_#{type}_association") << :autosave
137 end
138 end
139 end
140
141 module ClassMethods
142 private
143
144 # def belongs_to(name, options = {})
145 # super
146 # add_autosave_association_callbacks(reflect_on_association(name))
147 # end
148 ASSOCIATION_TYPES.each do |type|
149 module_eval %{
150 def #{type}(name, options = {})
151 super
152 add_autosave_association_callbacks(reflect_on_association(name))
153 end
154 }
155 end
156
157 # Adds a validate and save callback for the association as specified by
158 # the +reflection+.
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
163
164 case reflection.macro
165 when :has_many, :has_and_belongs_to_many
166 before_save :before_save_collection_association
167
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
172
173 define_method(validation_method) { validate_collection_association(reflection) }
174 else
175 case reflection.macro
176 when :has_one
177 define_method(save_method) { save_has_one_association(reflection) }
178 after_save save_method
179 when :belongs_to
180 define_method(save_method) { save_belongs_to_association(reflection) }
181 before_save save_method
182 end
183 define_method(validation_method) { validate_single_association(reflection) }
184 end
185 end
186 end
187
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)
192 end
193
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.
196 #
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
200 end
201
202 # Returns whether or not this record will be destroyed as part of the parents save transaction.
203 #
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
207 end
208
209 private
210
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)
215 if new_record
216 association
217 elsif association.loaded?
218 autosave ? association : association.select { |record| record.new_record? }
219 else
220 autosave ? association.target : association.target.select { |record| record.new_record? }
221 end
222 end
223
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)
230 end
231 end
232 end
233
234 # Validate the associated records if <tt>:validate</tt> or
235 # <tt>:autosave</tt> is turned on for the association specified by
236 # +reflection+.
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) }
241 end
242 end
243 end
244
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)
255 end
256 end
257 else
258 errors.add(reflection.name)
259 end
260 end
261 valid
262 end
263
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?
268 true
269 end
270
271 # Saves any new associated records, or all loaded autosave associations if
272 # <tt>:autosave</tt> is enabled on the association.
273 #
274 # In addition, it destroys all children that were marked for destruction
275 # with mark_for_destruction.
276 #
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]
282
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?
288 if autosave
289 association.send(:insert_record, record, false, false)
290 else
291 association.send(:insert_record, record)
292 end
293 elsif autosave
294 record.save(false)
295 end
296 end
297 end
298
299 # reconstruct the SQL queries now that we know the owner's id
300 association.send(:construct_sql) if association.respond_to?(:construct_sql)
301 end
302 end
303
304 # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
305 # on the association.
306 #
307 # In addition, it will destroy the association if it was marked for
308 # destruction with mark_for_destruction.
309 #
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?
315 association.destroy
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)
319 end
320 end
321 end
322
323 # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
324 # on the association.
325 #
326 # In addition, it will destroy the association if it was marked for
327 # destruction with mark_for_destruction.
328 #
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?
334 association.destroy
335 else
336 association.save(false) if association.new_record? || reflection.options[:autosave]
337
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
343 end
344 end
345 end
346 end
347 end
348 end
349 end