lib/activerecord-import/synchronize.rb



module ActiveRecord # :nodoc:
  class Base # :nodoc:
    # Synchronizes the passed in ActiveRecord instances with data
    # from the database. This is like calling reload on an individual
    # ActiveRecord instance but it is intended for use on multiple instances.
    #
    # This uses one query for all instance updates and then updates existing
    # instances rather sending one query for each instance
    #
    # == Examples
    # # Synchronizing existing models by matching on the primary key field
    # posts = Post.where(author: "Zach").first
    # <.. out of system changes occur to change author name from Zach to Zachary..>
    # Post.synchronize posts
    # posts.first.author # => "Zachary" instead of Zach
    #
    # # Synchronizing using custom key fields
    # posts = Post.where(author: "Zach").first
    # <.. out of system changes occur to change the address of author 'Zach' to 1245 Foo Ln ..>
    # Post.synchronize posts, [:name] # queries on the :name column and not the :id column
    # posts.first.address # => "1245 Foo Ln" instead of whatever it was
    #
    def self.synchronize(instances, keys = [primary_key])
      return if instances.empty?

      conditions = {}

      key_values = keys.map { |key| instances.map(&key.to_sym) }
      keys.zip(key_values).each { |key, values| conditions[key] = values }
      order = keys.map { |key| "#{key} ASC" }.join(",")

      klass = instances.first.class

      fresh_instances = klass.where(conditions).order(order)
      instances.each do |instance|
        matched_instance = fresh_instances.detect do |fresh_instance|
          keys.all? { |key| fresh_instance.send(key) == instance.send(key) }
        end

        next unless matched_instance

        instance.send :clear_aggregation_cache
        instance.send :clear_association_cache
        instance.instance_variable_set :@attributes, matched_instance.instance_variable_get(:@attributes)

        if instance.respond_to?(:clear_changes_information)
          instance.clear_changes_information                      # Rails 4.2 and higher
        else
          instance.instance_variable_set :@attributes_cache, {}   # Rails 4.0, 4.1
          instance.changed_attributes.clear                       # Rails 3.2
          instance.previous_changes.clear
        end

        # Since the instance now accurately reflects the record in
        # the database, ensure that instance.persisted? is true.
        instance.instance_variable_set '@new_record', false
        instance.instance_variable_set '@destroyed', false
      end
    end

    # See ActiveRecord::ConnectionAdapters::AbstractAdapter.synchronize
    def synchronize(instances, key = [ActiveRecord::Base.primary_key])
      self.class.synchronize(instances, key)
    end
  end
end