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

3
module ActiveRecord
4 5
  # = Active Record Autosave Association
  # 
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
  # AutosaveAssociation is a module that takes care of automatically saving
  # your associations when the parent is saved. In addition to saving, it
  # also destroys any associations that were marked for destruction.
  # (See mark_for_destruction and marked_for_destruction?)
  #
  # Saving of the parent, its associations, and the destruction of marked
  # associations, all happen inside 1 transaction. This should never leave the
  # database in an inconsistent state after, for instance, mass assigning
  # attributes and saving them.
  #
  # 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.
  #
  # === One-to-one Example
  #
  # Consider a Post model with one Author:
  #
  #   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 # => "The current global position of migrating ducks"
  #   post.author.name # => "alloy"
  #
  #   post.title = "On the migration of ducks"
  #   post.author.name = "Eloy Duran"
  #
  #   post.save
  #   post.reload
  #   post.title # => "On the migration of ducks"
  #   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:
  #   id = post.author.id
  #   Author.find_by_id(id).nil? # => false
  #
  #   post.save
  #   post.reload.author # => nil
  #
  # Now it _is_ removed from the database:
  #   Author.find_by_id(id).nil? # => true
  #
  # === One-to-many Example
  #
  # Consider a Post model with many Comments:
  #
  #   class Post
  #     has_many :comments, :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 # => "The current global position of migrating ducks"
  #   post.comments.first.body # => "Wow, awesome info thanks!"
  #   post.comments.last.body # => "Actually, your article should be named differently."
  #
  #   post.title = "On the migration of ducks"
  #   post.comments.last.body = "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
  #
  #   post.save
  #   post.reload
  #   post.title # => "On the migration of ducks"
  #   post.comments.last.body # => "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
  #
  # Destroying one of the associated models members, as part of the parent's
  # save action, is as simple as marking it for destruction:
  #
  #   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:
  #   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:
  #   Comment.find_by_id(id).nil? # => true
  #
  # === Validation
  #
  # Validation is performed on the parent as usual, but also on all autosave
  # enabled associations. If any of the associations fail validation, its
  # error messages will be applied on the parents errors object and validation
  # of the parent will fail.
  #
  # Consider a Post model with Author which validates the presence of its name
  # attribute:
  #
  #   class Post
  #     has_one :author, :autosave => true
  #   end
  #
  #   class Author
  #     validates_presence_of :name
  #   end
  #
  #   post = Post.find(1)
  #   post.author.name = ''
  #   post.save # => false
123
  #   post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author.name"=>["can't be blank"]}, @base=#<Post ...>>
124 125 126 127 128 129
  #
  # No validations will be performed on the associated models when validations
  # are skipped for the parent:
  #
  #   post = Post.find(1)
  #   post.author.name = ''
130
  #   post.save(:validate => false) # => true
131
  module AutosaveAssociation
132
    extend ActiveSupport::Concern
133

134 135
    ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many }

136 137 138
    included do
      ASSOCIATION_TYPES.each do |type|
        send("valid_keys_for_#{type}_association") << :autosave
139 140 141
      end
    end

142 143 144 145 146 147 148 149
    module ClassMethods
      private

      # def belongs_to(name, options = {})
      #   super
      #   add_autosave_association_callbacks(reflect_on_association(name))
      # end
      ASSOCIATION_TYPES.each do |type|
150
        module_eval <<-CODE, __FILE__, __LINE__ + 1
151 152 153
          def #{type}(name, options = {})
            super
            add_autosave_association_callbacks(reflect_on_association(name))
154
          end
155
        CODE
156 157
      end

158 159
      # Adds a validate and save callback for the association as specified by
      # the +reflection+.
160 161 162
      #
      # For performance reasons, we don't check whether to validate at runtime,
      # but instead only define the method and callback when needed. However,
163 164 165 166 167
      # 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.
168
      def add_autosave_association_callbacks(reflection)
169 170
        save_method = :"autosave_associated_records_for_#{reflection.name}"
        validation_method = :"validate_associated_records_for_#{reflection.name}"
171
        collection = reflection.collection?
172

173 174
        unless method_defined?(save_method)
          if collection
175
            before_save :before_save_collection_association
176

177 178 179 180
            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
181 182
          else
            if reflection.macro == :has_one
183 184
              define_method(save_method) { save_has_one_association(reflection) }
              after_save save_method
185
            else
186 187 188
              define_method(save_method) { save_belongs_to_association(reflection) }
              before_save save_method
            end
189
          end
190
        end
191

192 193 194 195
        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
196 197 198 199 200
        end
      end
    end

    # Reloads the attributes of the object as usual and removes a mark for destruction.
201
    def reload(options = nil)
202
      @marked_for_destruction = false
203
      super
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
    end

    # Marks this record to be destroyed as part of the parents save transaction.
    # This does _not_ actually destroy the record yet, rather it will be destroyed when <tt>parent.save</tt> is called.
    #
    # 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
220

221 222 223 224 225 226
    # 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?
      new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
    end
    
227 228 229 230 231 232 233 234
    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
235
      elsif autosave
236
        association.target.find_all { |record| record.changed_for_autosave? }
237
      else
238
        association.target.find_all { |record| record.new_record? }
239 240 241
      end
    end

242 243 244
    # 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?
245 246 247
      self.class.reflect_on_all_autosave_associations.any? do |reflection|
        association = association_instance_get(reflection.name)
        association && Array.wrap(association.target).any?(&:changed_for_autosave?)
248 249 250
      end
    end
    
251 252 253
    # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
    # turned on for the association specified by +reflection+.
    def validate_single_association(reflection)
254 255
      if (association = association_instance_get(reflection.name)) && !association.target.nil?
        association_valid?(reflection, association)
256 257 258 259 260 261 262
      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)
263
      if association = association_instance_get(reflection.name)
264 265 266 267 268 269 270
        if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
          records.each { |record| association_valid?(reflection, record) }
        end
      end
    end

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

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

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

            raise ActiveRecord::Rollback if saved == false
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
          end
        end

        # reconstruct the SQL queries now that we know the owner's id
        association.send(:construct_sql) if association.respond_to?(:construct_sql)
      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)
343
      if (association = association_instance_get(reflection.name)) && !association.target.nil? && !association.destroyed?
344 345 346
        autosave = reflection.options[:autosave]

        if autosave && association.marked_for_destruction?
347
          association.destroy
348 349 350 351
        else
          key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
          if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
            association[reflection.primary_key_name] = key
352
            saved = association.save(:validate => !autosave)
353 354
            raise ActiveRecord::Rollback if !saved && autosave
            saved
355
          end
356 357 358 359 360 361 362 363 364 365 366 367 368
        end
      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_belongs_to_association(reflection)
369
      if (association = association_instance_get(reflection.name)) && !association.destroyed?
370 371 372
        autosave = reflection.options[:autosave]

        if autosave && association.marked_for_destruction?
373
          association.destroy
374
        elsif autosave != false
375 376 377
          if association.new_record? || ( autosave && association.changed? )
            saved = association.save(:validate => !autosave) 
          end
378 379

          if association.updated?
380 381
            association_id = association.send(reflection.options[:primary_key] || :id)
            self[reflection.primary_key_name] = association_id
382
            # TODO: Removing this code doesn't seem to matter...
383 384 385 386
            if reflection.options[:polymorphic]
              self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
            end
          end
387 388

          saved if autosave
389 390 391
        end
      end
    end
392 393
  end
end