module JunkDrawer::BulkUpdatable

module to allow bulk updates for `ActiveRecord` models

Public Instance Methods

bulk_update(objects) click to toggle source
# File lib/junk_drawer/rails/bulk_updatable.rb, line 10
def bulk_update(objects)
  objects = objects.select(&:changed?)
  return unless objects.any?

  unique_objects = uniquify_and_merge(objects)
  changed_attributes = extract_changed_attributes(unique_objects)
  query = build_query_for(unique_objects, changed_attributes)
  connection.execute(query)
  objects.each(&:clear_changes_information)
end

Private Instance Methods

build_query_for(objects, attributes) click to toggle source
# File lib/junk_drawer/rails/bulk_updatable.rb, line 51
def build_query_for(objects, attributes)
  object_values = objects.map do |object|
    sanitized_values(object, attributes)
  end.join(', ')

  assignment_query = attributes.map do |attribute|
    quoted_column_name = connection.quote_column_name(attribute)
    "#{quoted_column_name} = tmp_table.#{quoted_column_name}"
  end.join(', ')

  "UPDATE #{table_name} " \
  "SET #{assignment_query} " \
  "FROM (VALUES #{object_values}) " \
  "AS tmp_table(id, #{attributes.join(', ')}) " \
  "WHERE #{table_name}.id = tmp_table.id"
end
extract_changed_attributes(objects) click to toggle source
# File lib/junk_drawer/rails/bulk_updatable.rb, line 38
def extract_changed_attributes(objects)
  now = Time.zone.now
  objects.each { |object| object.updated_at = now }

  changed_attributes = objects.flat_map(&:changed).uniq
  if ::ActiveRecord::VERSION::MAJOR >= 5
    column_names & changed_attributes
  else
    # to remove virtual columns from jsonb_accessor 0.3.3
    columns.select(&:sql_type).map(&:name) & changed_attributes
  end
end
sanitized_values(object, attributes) click to toggle source
# File lib/junk_drawer/rails/bulk_updatable.rb, line 68
def sanitized_values(object, attributes)
  postgres_values = attributes.map do |attribute|
    value = object[attribute]

    # AR internal `columns_hash`
    column = columns_hash[attribute.to_s]

    # AR internal `type_for_attribute`
    type = type_for_attribute(column.name)
    type_cast = "::#{column.sql_type}"
    type_cast = "#{type_cast}[]" if column.array

    "#{connection.quote(serialized_value(type, value))}#{type_cast}"
  end

  "(#{[object.id, *postgres_values].join(', ')})"
end
serialized_value(type, value) click to toggle source
# File lib/junk_drawer/rails/bulk_updatable.rb, line 86
def serialized_value(type, value)
  if ::ActiveRecord::VERSION::MAJOR >= 5
    type.serialize(value)
  else
    type.type_cast_for_database(value)
  end
end
uniquify_and_merge(objects) click to toggle source
# File lib/junk_drawer/rails/bulk_updatable.rb, line 23
def uniquify_and_merge(objects)
  grouped_objects = objects.group_by(&:id).values
  grouped_objects.each do |group|
    next if group.length == 1

    attrs = group.each_with_object({}) do |object, changes|
      object.changed.each do |changed_attribute|
        changes[changed_attribute] = object[changed_attribute]
      end
    end
    group.each { |object| object.attributes = attrs }
  end
  grouped_objects.map(&:first)
end