autosave_association.rb 18.9 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 #marked_for_destruction?).
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.
  #
A
AvnerCohen 已提交
19
  # Note that <tt>autosave: false</tt> is not same as not declaring <tt>:autosave</tt>.
20 21
  # When the <tt>:autosave</tt> option is not present then new association records are
  # saved but the updated association records are not saved.
22
  #
23 24
  # == Validation
  #
25
  # Child records are validated unless <tt>:validate</tt> is +false+.
26 27 28 29 30 31
  #
  # == 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
32
  # model. You should avoid modifying the association content, before
33 34 35
  # autosave callbacks are executed. Placing your callbacks after
  # associations is usually a good practice.
  #
36 37
  # === One-to-one Example
  #
38
  #   class Post < ActiveRecord::Base
A
AvnerCohen 已提交
39
  #     has_one :author, autosave: true
40 41 42 43 44 45
  #   end
  #
  # Saving changes to the parent and its associated model can now be performed
  # automatically _and_ atomically:
  #
  #   post = Post.find(1)
46 47 48
  #   post.title       # => "The current global position of migrating ducks"
  #   post.author.name # => "alloy"
  #
49 50 51 52 53
  #   post.title = "On the migration of ducks"
  #   post.author.name = "Eloy Duran"
  #
  #   post.save
  #   post.reload
54
  #   post.title       # => "On the migration of ducks"
55 56 57 58 59 60 61 62 63
  #   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:
64
  #
65
  #   id = post.author.id
66
  #   Author.find_by(id: id).nil? # => false
67 68 69 70 71
  #
  #   post.save
  #   post.reload.author # => nil
  #
  # Now it _is_ removed from the database:
72
  #
73
  #   Author.find_by(id: id).nil? # => true
74 75 76
  #
  # === One-to-many Example
  #
77
  # When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
78
  #
79
  #   class Post < ActiveRecord::Base
80
  #     has_many :comments # :autosave option is not declared
81 82
  #   end
  #
A
AvnerCohen 已提交
83 84
  #   post = Post.new(title: 'ruby rocks')
  #   post.comments.build(body: 'hello world')
85
  #   post.save # => saves both post and comment
86
  #
A
AvnerCohen 已提交
87 88
  #   post = Post.create(title: 'ruby rocks')
  #   post.comments.build(body: 'hello world')
89
  #   post.save # => saves both post and comment
90
  #
A
AvnerCohen 已提交
91 92
  #   post = Post.create(title: 'ruby rocks')
  #   post.comments.create(body: 'hello world')
93
  #   post.save # => saves both post and comment
94
  #
95 96
  # When <tt>:autosave</tt> is true all children are saved, no matter whether they
  # are new records or not:
97
  #
98
  #   class Post < ActiveRecord::Base
A
AvnerCohen 已提交
99
  #     has_many :comments, autosave: true
100 101
  #   end
  #
A
AvnerCohen 已提交
102 103
  #   post = Post.create(title: 'ruby rocks')
  #   post.comments.create(body: 'hello world')
104
  #   post.comments[0].body = 'hi everyone'
105 106 107
  #   post.comments.build(body: "good morning.")
  #   post.title += "!"
  #   post.save # => saves both post and comments.
108
  #
109 110
  # Destroying one of the associated models as part of the parent's save action
  # is as simple as marking it for destruction:
111
  #
112 113 114
  #   post.comments # => [#<Comment id: 1, ...>, #<Comment id: 2, ...]>
  #   post.comments[1].mark_for_destruction
  #   post.comments[1].marked_for_destruction? # => true
115 116 117
  #   post.comments.length # => 2
  #
  # Note that the model is _not_ yet removed from the database:
118
  #
119
  #   id = post.comments.last.id
120
  #   Comment.find_by(id: id).nil? # => false
121 122 123 124 125
  #
  #   post.save
  #   post.reload.comments.length # => 1
  #
  # Now it _is_ removed from the database:
126
  #
127
  #   Comment.find_by(id: id).nil? # => true
128
  module AutosaveAssociation
129
    extend ActiveSupport::Concern
130

131
    module AssociationBuilderExtension #:nodoc:
132
      def self.build(model, reflection)
133
        model.send(:add_autosave_association_callbacks, reflection)
134 135 136 137
      end

      def self.valid_options
        [ :autosave ]
138 139
      end
    end
140

141
    included do
142
      Associations::Builder::Association.extensions << AssociationBuilderExtension
143 144
      mattr_accessor :index_nested_attribute_errors, instance_writer: false
      self.index_nested_attribute_errors = false
145 146
    end

147
    module ClassMethods # :nodoc:
148 149
      private

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

164 165
            result
          end
166 167
        end

168 169 170 171 172 173 174 175 176 177 178 179 180 181
        # Adds validation and save callbacks for the association as specified by
        # the +reflection+.
        #
        # For performance reasons, we don't check whether to validate at runtime.
        # However the validation and callback methods are lazy and those methods
        # get created when they are invoked for the very first time. However,
        # 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.
        def add_autosave_association_callbacks(reflection)
          save_method = :"autosave_associated_records_for_#{reflection.name}"

182
          if reflection.collection?
183
            before_save :before_save_collection_association
184

185
            define_non_cyclic_method(save_method) { save_collection_association(reflection) }
186 187 188
            # 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
189
          elsif reflection.has_one?
190 191 192 193 194 195 196 197 198 199 200 201
            define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method)
            # 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
          else
202
            define_non_cyclic_method(save_method) { throw(:abort) if save_belongs_to_association(reflection) == false }
203
            before_save save_method
204
          end
205

206 207 208 209 210
          define_autosave_validation_callbacks(reflection)
        end

        def define_autosave_validation_callbacks(reflection)
          validation_method = :"validate_associated_records_for_#{reflection.name}"
211
          if reflection.validate? && !method_defined?(validation_method)
212 213 214 215 216 217
            if reflection.collection?
              method = :validate_collection_association
            else
              method = :validate_single_association
            end

218 219 220 221 222 223 224
            define_non_cyclic_method(validation_method) do
              send(method, reflection)
              # TODO: remove the following line as soon as the return value of
              # callbacks is ignored, that is, returning `false` does not
              # display a deprecation warning or halts the callback chain.
              true
            end
225
            validate validation_method
226
            after_validation :_ensure_no_duplicate_errors
227
          end
228 229 230
        end
    end

N
Neeraj Singh 已提交
231
    # Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
232
    def reload(options = nil)
233
      @marked_for_destruction = false
234
      @destroyed_by_association = nil
235
      super
236 237
    end

238
    # Marks this record to be destroyed as part of the parent's save transaction.
239
    # This does _not_ actually destroy the record instantly, rather child record will be destroyed
240
    # when <tt>parent.save</tt> is called.
241 242 243 244 245 246
    #
    # 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

247
    # Returns whether or not this record will be destroyed as part of the parent's save transaction.
248 249 250 251 252
    #
    # 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
253

254 255 256 257 258 259 260 261 262 263 264 265 266
    # Records the association that is being destroyed and destroying this
    # record in the process.
    def destroyed_by_association=(reflection)
      @destroyed_by_association = reflection
    end

    # Returns the association for the parent being destroyed.
    #
    # Used to avoid updating the counter cache unnecessarily.
    def destroyed_by_association
      @destroyed_by_association
    end

267 268 269
    # 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?
270
      new_record? || has_changes_to_save? || marked_for_destruction? || nested_records_changed_for_autosave?
271
    end
272

273 274
    private

275 276 277 278 279 280 281
      # 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
          association && association.target
        elsif autosave
282
          association.target.find_all(&:changed_for_autosave?)
283
        else
284
          association.target.find_all(&:new_record?)
285
        end
286 287
      end

288 289 290
      # 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?
291 292 293 294 295 296 297 298 299
        @_nested_records_changed_for_autosave_already_called ||= false
        return false if @_nested_records_changed_for_autosave_already_called
        begin
          @_nested_records_changed_for_autosave_already_called = true
          self.class._reflections.values.any? do |reflection|
            if reflection.options[:autosave]
              association = association_instance_get(reflection.name)
              association && Array.wrap(association.target).any?(&:changed_for_autosave?)
            end
300
          end
301 302
        ensure
          @_nested_records_changed_for_autosave_already_called = false
303
        end
304
      end
305

306 307 308 309 310 311 312
      # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
      # turned on for the association.
      def validate_single_association(reflection)
        association = association_instance_get(reflection.name)
        record      = association && association.reader
        association_valid?(reflection, record) if record
      end
313

314 315 316 317 318 319
      # 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)
        if association = association_instance_get(reflection.name)
          if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
320
            records.each_with_index { |record, index| association_valid?(reflection, record, index) }
321
          end
322 323 324
        end
      end

325 326 327
      # Returns whether or not the association is valid and applies any errors to
      # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
      # enabled records if they're marked_for_destruction? or destroyed.
328
      def association_valid?(reflection, record, index = nil)
329
        return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?)
330

331
        validation_context = self.validation_context unless [:create, :update].include?(self.validation_context)
332

333
        unless valid = record.valid?(validation_context)
334
          if reflection.options[:autosave]
335 336
            indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors)

337
            record.errors.each do |attribute, message|
338
              attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
339 340 341
              errors[attribute] << message
              errors[attribute].uniq!
            end
342 343

            record.errors.details.each_key do |attribute|
344 345
              reflection_attribute =
                normalize_reflection_attribute(indexed_attribute, reflection, index, attribute).to_sym
346 347 348 349 350 351

              record.errors.details[attribute].each do |error|
                errors.details[reflection_attribute] << error
                errors.details[reflection_attribute].uniq!
              end
            end
352 353
          else
            errors.add(reflection.name)
354 355
          end
        end
356
        valid
357 358
      end

359 360 361 362 363 364 365 366
      def normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
        if indexed_attribute
          "#{reflection.name}[#{index}].#{attribute}"
        else
          "#{reflection.name}.#{attribute}"
        end
      end

367 368 369 370 371 372
      # 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
        @new_record_before_save = new_record?
        true
      end
373

374 375 376 377
      # 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
378
      # with #mark_for_destruction.
379 380 381 382 383 384 385
      #
      # 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]

386 387 388
          # reconstruct the scope now that we know the owner's id
          association.reset_scope if association.respond_to?(:reset_scope)

389 390 391 392 393 394
          if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
            if autosave
              records_to_destroy = records.select(&:marked_for_destruction?)
              records_to_destroy.each { |record| association.destroy(record) }
              records -= records_to_destroy
            end
395

396 397
            records.each do |record|
              next if record.destroyed?
398

399
              saved = true
400

401 402 403 404 405 406 407
              if autosave != false && (@new_record_before_save || record.new_record?)
                if autosave
                  saved = association.insert_record(record, false)
                else
                  association.insert_record(record) unless reflection.nested?
                end
              elsif autosave
408
                saved = record.save(validate: false)
409
              end
410

411 412
              raise ActiveRecord::Rollback unless saved
            end
413
          end
414
        end
415 416
      end

417 418 419 420
      # 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
421
      # destruction with #mark_for_destruction.
422 423 424 425 426 427
      #
      # 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)
        association = association_instance_get(reflection.name)
        record      = association && association.load_target
428

429 430
        if record && !record.destroyed?
          autosave = reflection.options[:autosave]
431

432 433
          if autosave && record.marked_for_destruction?
            record.destroy
434
          elsif autosave != false
435
            key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
436

437
            if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key)
438 439 440 441
              unless reflection.through_reflection
                record[reflection.foreign_key] = key
              end

442
              saved = record.save(validate: !autosave)
443 444 445
              raise ActiveRecord::Rollback if !saved && autosave
              saved
            end
446
          end
447 448 449
        end
      end

450 451
      # If the record is new or it has changed, returns true.
      def record_changed?(reflection, record, key)
452
        record.new_record? ||
R
Rafael Mendonça França 已提交
453
          (record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) ||
454
          record.will_save_change_to_attribute?(reflection.foreign_key)
455 456
      end

457 458 459 460 461
      # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
      #
      # In addition, it will destroy the association if it was marked for destruction.
      def save_belongs_to_association(reflection)
        association = association_instance_get(reflection.name)
462 463 464
        return unless association && association.loaded? && !association.stale_target?

        record = association.load_target
465 466 467 468 469 470 471
        if record && !record.destroyed?
          autosave = reflection.options[:autosave]

          if autosave && record.marked_for_destruction?
            self[reflection.foreign_key] = nil
            record.destroy
          elsif autosave != false
472
            saved = record.save(validate: !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
473 474 475 476 477 478

            if association.updated?
              association_id = record.send(reflection.options[:primary_key] || :id)
              self[reflection.foreign_key] = association_id
              association.loaded!
            end
479

480 481
            saved if autosave
          end
482 483
        end
      end
484 485 486 487 488 489

      def _ensure_no_duplicate_errors
        errors.messages.each_key do |attribute|
          errors[attribute].uniq!
        end
      end
490
  end
491
end