autosave_association.rb 15.5 KB
Newer Older
1
module ActiveRecord
2
  # = Active Record Autosave Association
3
  #
4
  # +AutosaveAssociation+ is a module that takes care of automatically saving
R
R.T. Lechow 已提交
5
  # associated records when their parent is saved. In addition to saving, it
6
  # also destroys any associated records that were marked for destruction.
7
  # (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>).
8 9
  #
  # Saving of the parent, its associations, and the destruction of marked
10
  # associations, all happen inside a transaction. This should never leave the
11
  # database in an inconsistent state.
12 13 14 15 16 17 18
  #
  # If validations for any of the associations fail, their error messages will
  # be applied to the parent.
  #
  # Note that it also means that associations marked for destruction won't
  # be destroyed directly. They will however still be marked for destruction.
  #
19 20
  # Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>.
  # When the <tt>:autosave</tt> option is not present new associations are saved.
21
  #
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
  # == Validation
  #
  # Children records are validated unless <tt>:validate</tt> is +false+.
  #
  # == Callbacks
  #
  # Association with autosave option defines several callbacks on your
  # model (before_save, after_create, after_update). Please note that
  # callbacks are executed in the order they were defined in
  # model. You should avoid modyfing the association content, before
  # autosave callbacks are executed. Placing your callbacks after
  # associations is usually a good practice.
  #
  # == Examples
  #
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
  # === One-to-one Example
  #
  #   class Post
  #     has_one :author, :autosave => true
  #   end
  #
  # Saving changes to the parent and its associated model can now be performed
  # automatically _and_ atomically:
  #
  #   post = Post.find(1)
  #   post.title = "On the migration of ducks"
  #   post.author.name = "Eloy Duran"
  #
  #   post.save
  #   post.reload
52
  #   post.title       # => "On the migration of ducks"
53 54 55 56 57 58 59 60 61
  #   post.author.name # => "Eloy Duran"
  #
  # Destroying an associated model, as part of the parent's save action, is as
  # simple as marking it for destruction:
  #
  #   post.author.mark_for_destruction
  #   post.author.marked_for_destruction? # => true
  #
  # Note that the model is _not_ yet removed from the database:
62
  #
63 64 65 66 67 68 69
  #   id = post.author.id
  #   Author.find_by_id(id).nil? # => false
  #
  #   post.save
  #   post.reload.author # => nil
  #
  # Now it _is_ removed from the database:
70
  #
71 72 73 74
  #   Author.find_by_id(id).nil? # => true
  #
  # === One-to-many Example
  #
75
  # When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
76 77
  #
  #   class Post
78
  #     has_many :comments # :autosave option is not declared
79 80
  #   end
  #
81 82
  #   post = Post.new(:title => 'ruby rocks')
  #   post.comments.build(:body => 'hello world')
83
  #   post.save # => saves both post and comment
84
  #
85 86
  #   post = Post.create(:title => 'ruby rocks')
  #   post.comments.build(:body => 'hello world')
87
  #   post.save # => saves both post and comment
88
  #
89 90
  #   post = Post.create(:title => 'ruby rocks')
  #   post.comments.create(:body => 'hello world')
91
  #   post.save # => saves both post and comment
92
  #
93 94
  # When <tt>:autosave</tt> is true all children are saved, no matter whether they
  # are new records or not:
95 96 97 98 99 100 101 102
  #
  #   class Post
  #     has_many :comments, :autosave => true
  #   end
  #
  #   post = Post.create(:title => 'ruby rocks')
  #   post.comments.create(:body => 'hello world')
  #   post.comments[0].body = 'hi everyone'
R
Ray Baxter 已提交
103
  #   post.save # => saves both post and comment, with 'hi everyone' as body
104
  #
105 106
  # Destroying one of the associated models as part of the parent's save action
  # is as simple as marking it for destruction:
107 108 109 110 111 112
  #
  #   post.comments.last.mark_for_destruction
  #   post.comments.last.marked_for_destruction? # => true
  #   post.comments.length # => 2
  #
  # Note that the model is _not_ yet removed from the database:
113
  #
114 115 116 117 118 119 120
  #   id = post.comments.last.id
  #   Comment.find_by_id(id).nil? # => false
  #
  #   post.save
  #   post.reload.comments.length # => 1
  #
  # Now it _is_ removed from the database:
121
  #
122
  #   Comment.find_by_id(id).nil? # => true
123

124
  module AutosaveAssociation
125
    extend ActiveSupport::Concern
126

127 128 129 130 131 132 133 134 135 136 137 138 139
    ASSOCIATION_TYPES = %w{ HasOne HasMany BelongsTo HasAndBelongsToMany }

    module AssociationBuilderExtension #:nodoc:
      def self.included(base)
        base.valid_options << :autosave
      end

      def build
        reflection = super
        model.send(:add_autosave_association_callbacks, reflection)
        reflection
      end
    end
140

141 142
    included do
      ASSOCIATION_TYPES.each do |type|
143
        Associations::Builder.const_get(type).send(:include, AssociationBuilderExtension)
144 145 146
      end
    end

147 148 149
    module ClassMethods
      private

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
      def define_non_cyclic_method(name, reflection, &block)
        define_method(name) do |*args|
          result = true; @_already_called ||= {}
          # Loop prevention for validation of associations
          unless @_already_called[[name, reflection.name]]
            begin
              @_already_called[[name, reflection.name]]=true
              result = instance_eval(&block)
            ensure
              @_already_called[[name, reflection.name]]=false
            end
          end

          result
        end
      end

167
      # Adds validation and save callbacks for the association as specified by
168
      # the +reflection+.
169
      #
170 171
      # For performance reasons, we don't check whether to validate at runtime.
      # However the validation and callback methods are lazy and those methods
172
      # get created when they are invoked for the very first time. However,
173 174 175 176 177
      # this can change, for instance, when using nested attributes, which is
      # called _after_ the association has been defined. Since we don't want
      # the callbacks to get defined multiple times, there are guards that
      # check if the save or validation methods have already been defined
      # before actually defining them.
178
      def add_autosave_association_callbacks(reflection)
179 180
        save_method = :"autosave_associated_records_for_#{reflection.name}"
        validation_method = :"validate_associated_records_for_#{reflection.name}"
181
        collection = reflection.collection?
182

183 184
        unless method_defined?(save_method)
          if collection
185
            before_save :before_save_collection_association
186

187
            define_non_cyclic_method(save_method, reflection) { save_collection_association(reflection) }
188 189 190
            # Doesn't use after_save as that would save associations added in after_create/after_update twice
            after_create save_method
            after_update save_method
191 192
          else
            if reflection.macro == :has_one
193
              define_method(save_method) { save_has_one_association(reflection) }
194 195 196 197 198 199 200 201 202 203
              # Configures two callbacks instead of a single after_save so that
              # the model may rely on their execution order relative to its
              # own callbacks.
              #
              # For example, given that after_creates run before after_saves, if
              # we configured instead an after_save there would be no way to fire
              # a custom after_create callback after the child association gets
              # created.
              after_create save_method
              after_update save_method
204
            else
205
              define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
206 207
              before_save save_method
            end
208
          end
209
        end
210

211 212
        if reflection.validate? && !method_defined?(validation_method)
          method = (collection ? :validate_collection_association : :validate_single_association)
213
          define_non_cyclic_method(validation_method, reflection) { send(method, reflection) }
214
          validate validation_method
215 216 217 218
        end
      end
    end

N
Neeraj Singh 已提交
219
    # Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
220
    def reload(options = nil)
221
      @marked_for_destruction = false
222
      super
223 224 225
    end

    # Marks this record to be destroyed as part of the parents save transaction.
226
    # This does _not_ actually destroy the record instantly, rather child record will be destroyed
227
    # when <tt>parent.save</tt> is called.
228 229 230 231 232 233 234 235 236 237 238 239
    #
    # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
    def mark_for_destruction
      @marked_for_destruction = true
    end

    # Returns whether or not this record will be destroyed as part of the parents save transaction.
    #
    # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
    def marked_for_destruction?
      @marked_for_destruction
    end
240

241 242 243
    # Returns whether or not this record has been changed in any way (including whether
    # any of its nested autosave associations are likewise changed)
    def changed_for_autosave?
244
      new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
245
    end
246

247 248 249 250 251 252 253
    private

    # Returns the record for an association collection that should be validated
    # or saved. If +autosave+ is +false+ only new records will be returned,
    # unless the parent is/was a new record itself.
    def associated_records_to_validate_or_save(association, new_record, autosave)
      if new_record
254
        association && association.target
255
      elsif autosave
256
        association.target.find_all { |record| record.changed_for_autosave? }
257
      else
258
        association.target.find_all { |record| record.new_record? }
259 260 261
      end
    end

262 263 264
    # go through nested autosave associations that are loaded in memory (without loading
    # any new ones), and return true if is changed for autosave
    def nested_records_changed_for_autosave?
265 266
      self.class.reflect_on_all_autosave_associations.any? do |reflection|
        association = association_instance_get(reflection.name)
267
        association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
268 269
      end
    end
270

271
    # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
272
    # turned on for the association.
273
    def validate_single_association(reflection)
274
      association = association_instance_get(reflection.name)
275
      record      = association && association.reader
276
      association_valid?(reflection, record) if record
277 278 279 280 281 282
    end

    # Validate the associated records if <tt>:validate</tt> or
    # <tt>:autosave</tt> is turned on for the association specified by
    # +reflection+.
    def validate_collection_association(reflection)
283
      if association = association_instance_get(reflection.name)
284
        if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
285 286 287 288 289 290
          records.each { |record| association_valid?(reflection, record) }
        end
      end
    end

    # Returns whether or not the association is valid and applies any errors to
291
    # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
292
    # enabled records if they're marked_for_destruction? or destroyed.
293 294
    def association_valid?(reflection, record)
      return true if record.destroyed? || record.marked_for_destruction?
295

296
      unless valid = record.valid?(validation_context)
297
        if reflection.options[:autosave]
298
          record.errors.each do |attribute, message|
299
            attribute = "#{reflection.name}.#{attribute}"
300 301
            errors[attribute] << message
            errors[attribute].uniq!
302 303 304 305 306 307 308 309 310 311 312
          end
        else
          errors.add(reflection.name)
        end
      end
      valid
    end

    # Is used as a before_save callback to check while saving a collection
    # association whether or not the parent was a new record before saving.
    def before_save_collection_association
313
      @new_record_before_save = new_record?
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329
      true
    end

    # Saves any new associated records, or all loaded autosave associations if
    # <tt>:autosave</tt> is enabled on the association.
    #
    # In addition, it destroys all children that were marked for destruction
    # with mark_for_destruction.
    #
    # This all happens inside a transaction, _if_ the Transactions module is included into
    # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
    def save_collection_association(reflection)
      if association = association_instance_get(reflection.name)
        autosave = reflection.options[:autosave]

        if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
330
          begin
331
          records.each do |record|
332 333
            next if record.destroyed?

334 335
            saved = true

336
            if autosave && record.marked_for_destruction?
337
              association.proxy.destroy(record)
338
            elsif autosave != false && (@new_record_before_save || record.new_record?)
339
              if autosave
340
                saved = association.insert_record(record, false)
341
              else
342
                association.insert_record(record) unless reflection.nested?
343 344
              end
            elsif autosave
345
              saved = record.save(:validate => false)
346
            end
347

348
            raise ActiveRecord::Rollback unless saved
349
          end
350 351 352
          rescue
            records.each {|x| IdentityMap.remove(x) } if IdentityMap.enabled?
            raise
353
          end
354

355 356
        end

357
        # reconstruct the scope now that we know the owner's id
358
        association.send(:reset_scope) if association.respond_to?(:reset_scope)
359 360 361 362 363 364 365 366 367 368 369 370
      end
    end

    # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
    # on the association.
    #
    # In addition, it will destroy the association if it was marked for
    # destruction with mark_for_destruction.
    #
    # This all happens inside a transaction, _if_ the Transactions module is included into
    # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
    def save_has_one_association(reflection)
371 372 373
      association = association_instance_get(reflection.name)
      record      = association && association.load_target
      if record && !record.destroyed?
374 375
        autosave = reflection.options[:autosave]

376 377
        if autosave && record.marked_for_destruction?
          record.destroy
378 379
        else
          key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
380
          if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave)
381 382 383 384
            unless reflection.through_reflection
              record[reflection.foreign_key] = key
            end

385
            saved = record.save(:validate => !autosave)
386 387
            raise ActiveRecord::Rollback if !saved && autosave
            saved
388
          end
389 390 391 392
        end
      end
    end

393
    # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
394
    #
395
    # In addition, it will destroy the association if it was marked for destruction.
396
    def save_belongs_to_association(reflection)
397 398 399
      association = association_instance_get(reflection.name)
      record      = association && association.load_target
      if record && !record.destroyed?
400 401
        autosave = reflection.options[:autosave]

402 403
        if autosave && record.marked_for_destruction?
          record.destroy
404
        elsif autosave != false
405
          saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
406 407

          if association.updated?
408
            association_id = record.send(reflection.options[:primary_key] || :id)
409
            self[reflection.foreign_key] = association_id
410
            association.loaded!
411
          end
412 413

          saved if autosave
414 415 416
        end
      end
    end
417
  end
418
end