autosave_association.rb 14.4 KB
Newer Older
1 2
require 'active_support/core_ext/array/wrap'

3
module ActiveRecord
4
  # = Active Record Autosave Association
5
  #
6 7
  # +AutosaveAssociation+ is a module that takes care of automatically saving
  # associacted records when their parent is saved. In addition to saving, it
8
  # also destroys any associated records that were marked for destruction.
9
  # (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>).
10 11
  #
  # Saving of the parent, its associations, and the destruction of marked
12
  # associations, all happen inside a transaction. This should never leave the
13
  # database in an inconsistent state.
14 15 16 17 18 19 20
  #
  # 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.
  #
21 22
  # 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.
23
  #
24 25 26 27 28 29 30 31 32 33
  # === 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)
34
  #   post.title       # => "The current global position of migrating ducks"
35 36 37 38 39 40 41
  #   post.author.name # => "alloy"
  #
  #   post.title = "On the migration of ducks"
  #   post.author.name = "Eloy Duran"
  #
  #   post.save
  #   post.reload
42
  #   post.title       # => "On the migration of ducks"
43 44 45 46 47 48 49 50 51
  #   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:
52
  #
53 54 55 56 57 58 59
  #   id = post.author.id
  #   Author.find_by_id(id).nil? # => false
  #
  #   post.save
  #   post.reload.author # => nil
  #
  # Now it _is_ removed from the database:
60
  #
61 62 63 64
  #   Author.find_by_id(id).nil? # => true
  #
  # === One-to-many Example
  #
65
  # When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
66 67
  #
  #   class Post
68
  #     has_many :comments # :autosave option is no declared
69 70
  #   end
  #
71 72
  #   post = Post.new(:title => 'ruby rocks')
  #   post.comments.build(:body => 'hello world')
73
  #   post.save # => saves both post and comment
74
  #
75 76
  #   post = Post.create(:title => 'ruby rocks')
  #   post.comments.build(:body => 'hello world')
77
  #   post.save # => saves both post and comment
78
  #
79 80
  #   post = Post.create(:title => 'ruby rocks')
  #   post.comments.create(:body => 'hello world')
81
  #   post.save # => saves both post and comment
82
  #
83
  # When <tt>:autosave</tt> is true all children is saved, no matter whether they are new records:
84 85 86 87 88 89 90 91
  #
  #   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 已提交
92
  #   post.save # => saves both post and comment, with 'hi everyone' as body
93
  #
94 95
  # Destroying one of the associated models as part of the parent's save action
  # is as simple as marking it for destruction:
96 97 98 99 100 101
  #
  #   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:
102
  #
103 104 105 106 107 108 109
  #   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:
110
  #
111 112 113 114
  #   Comment.find_by_id(id).nil? # => true
  #
  # === Validation
  #
115
  # Children records are validated unless <tt>:validate</tt> is +false+.
116
  module AutosaveAssociation
117
    extend ActiveSupport::Concern
118

119 120
    ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many }

121 122 123
    included do
      ASSOCIATION_TYPES.each do |type|
        send("valid_keys_for_#{type}_association") << :autosave
124 125 126
      end
    end

127 128 129 130 131 132 133 134
    module ClassMethods
      private

      # def belongs_to(name, options = {})
      #   super
      #   add_autosave_association_callbacks(reflect_on_association(name))
      # end
      ASSOCIATION_TYPES.each do |type|
135
        module_eval <<-CODE, __FILE__, __LINE__ + 1
136 137 138
          def #{type}(name, options = {})
            super
            add_autosave_association_callbacks(reflect_on_association(name))
139
          end
140
        CODE
141 142
      end

143
      # Adds validation and save callbacks for the association as specified by
144
      # the +reflection+.
145
      #
146 147 148
      # 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,
149 150 151 152 153
      # 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.
154
      def add_autosave_association_callbacks(reflection)
155 156
        save_method = :"autosave_associated_records_for_#{reflection.name}"
        validation_method = :"validate_associated_records_for_#{reflection.name}"
157
        collection = reflection.collection?
158

159 160
        unless method_defined?(save_method)
          if collection
161
            before_save :before_save_collection_association
162

163 164 165 166
            define_method(save_method) { save_collection_association(reflection) }
            # 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
167 168
          else
            if reflection.macro == :has_one
169
              define_method(save_method) { save_has_one_association(reflection) }
170 171 172 173 174 175 176 177 178 179
              # 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
180
            else
181 182 183
              define_method(save_method) { save_belongs_to_association(reflection) }
              before_save save_method
            end
184
          end
185
        end
186

187 188 189 190
        if reflection.validate? && !method_defined?(validation_method)
          method = (collection ? :validate_collection_association : :validate_single_association)
          define_method(validation_method) { send(method, reflection) }
          validate validation_method
191 192 193 194
        end
      end
    end

N
Neeraj Singh 已提交
195
    # Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
196
    def reload(options = nil)
197
      @marked_for_destruction = false
198
      super
199 200 201
    end

    # Marks this record to be destroyed as part of the parents save transaction.
202
    # This does _not_ actually destroy the record instantly, rather child record will be destroyed
203
    # when <tt>parent.save</tt> is called.
204 205 206 207 208 209 210 211 212 213 214 215
    #
    # 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
216

217 218 219
    # 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?
220
      new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
221
    end
222

223 224 225 226 227 228 229 230
    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
        association
231
      elsif autosave
232
        association.target.find_all { |record| record.changed_for_autosave? }
233
      else
234
        association.target.find_all { |record| record.new_record? }
235 236 237
      end
    end

238 239 240
    # 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?
241 242
      self.class.reflect_on_all_autosave_associations.any? do |reflection|
        association = association_instance_get(reflection.name)
243
        association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
244 245
      end
    end
246

247
    # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
248
    # turned on for the association.
249
    def validate_single_association(reflection)
250 251
      if (association = association_instance_get(reflection.name)) && !association.target.nil?
        association_valid?(reflection, association)
252 253 254 255 256 257 258
      end
    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)
259
      if association = association_instance_get(reflection.name)
260
        if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
261 262 263 264 265 266
          records.each { |record| association_valid?(reflection, record) }
        end
      end
    end

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

272 273
      unless valid = association.valid?
        if reflection.options[:autosave]
274
          association.errors.each do |attribute, message|
275
            attribute = "#{reflection.name}.#{attribute}"
276 277
            errors[attribute] << message
            errors[attribute].uniq!
278 279 280 281 282 283 284 285 286 287 288
          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
289
      @new_record_before_save = new_record?
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
      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)
          records.each do |record|
307 308
            next if record.destroyed?

309
            if autosave && record.marked_for_destruction?
310
              association.destroy(record)
311
            elsif autosave != false && (@new_record_before_save || record.new_record?)
312
              if autosave
313
                saved = association.send(:insert_record, record, false, false)
314 315 316 317
              else
                association.send(:insert_record, record)
              end
            elsif autosave
318
              saved = record.save(:validate => false)
319
            end
320 321

            raise ActiveRecord::Rollback if saved == false
322 323 324
          end
        end

325 326
        # reconstruct the scope now that we know the owner's id
        association.send(:construct_scope) if association.respond_to?(:construct_scope)
327 328 329 330 331 332 333 334 335 336 337 338
      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)
339
      if (association = association_instance_get(reflection.name)) && !association.target.nil? && !association.destroyed?
340 341 342
        autosave = reflection.options[:autosave]

        if autosave && association.marked_for_destruction?
343
          association.destroy
344 345
        else
          key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
346
          if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
347
            association[reflection.primary_key_name] = key
348
            saved = association.save(:validate => !autosave)
349 350
            raise ActiveRecord::Rollback if !saved && autosave
            saved
351
          end
352 353 354 355
        end
      end
    end

356
    # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
357
    #
358
    # In addition, it will destroy the association if it was marked for destruction.
359
    def save_belongs_to_association(reflection)
360
      if (association = association_instance_get(reflection.name)) && !association.destroyed?
361 362 363
        autosave = reflection.options[:autosave]

        if autosave && association.marked_for_destruction?
364
          association.destroy
365
        elsif autosave != false
366
          saved = association.save(:validate => !autosave) if association.new_record? || (autosave && association.changed_for_autosave?)
367 368

          if association.updated?
369 370
            association_id = association.send(reflection.options[:primary_key] || :id)
            self[reflection.primary_key_name] = association_id
371
          end
372 373

          saved if autosave
374 375 376
        end
      end
    end
377
  end
378
end