Module: ActiveRecord::UpdateInBulk::Relation

Included in:
Relation
Defined in:
lib/activerecord-updateinbulk/relation.rb

Instance Method Summary collapse

Instance Method Details

#update_in_bulk(updates, values = nil, record_timestamps: nil, formulas: nil) ⇒ Object

Updates multiple groups of records in the current relation using a single SQL UPDATE statement. This does not instantiate models and does not trigger Active Record callbacks or validations. However, values passed through still use Active Record’s normal type casting and serialization. Returns the number of rows affected.

Three equivalent input formats are supported for convenience:

Indexed format — a hash mapping primary keys to attribute updates:

Book.update_in_bulk({
  1 => { title: "Agile", price: 10.0 },
  2 => { title: "Rails" }
})

Composite primary keys are supported:

FlightSeat.update_in_bulk({
  ["AA100", "12A"] => { passenger: "Alice" },
  ["AA100", "12B"] => { passenger: "Bob" }
})

Paired format — an array of [conditions, assigns] pairs. Conditions do not need to be primary keys; they may reference any columns in the target table. All pairs must specify the same set of condition columns:

Employee.update_in_bulk([
  [{ department: "Sales" },       { bonus: 2500 }],
  [{ department: "Engineering" }, { bonus: 500 }]
])

Separated format — parallel arrays of conditions and assigns:

Employee.update_in_bulk(
  [1, 2, { id: 3 }],
  [{ salary: 75_000 }, { salary: 80_000 }, { salary: 68_000 }]
)

Options

:record_timestamps

By default, automatic setting of timestamp columns is controlled by the model’s record_timestamps config, matching typical behavior. Timestamps are only bumped when the row actually changes.

To override this and force automatic setting of timestamp columns one way or the other, pass :record_timestamps.

Pass record_timestamps: :always to always assign timestamp columns to the current database timestamp (without change-detection CASE logic).

:formulas

A hash of column names to formula identifiers or Procs. Instead of a simple assignment, the column is set to an expression that can reference both the current selected row value and the incoming value.

Built-in formulas: :add, :subtract, :concat_append, :concat_prepend.

Inventory.update_in_bulk({
  "Christmas balls" => { quantity: 73 },
  "Christmas tree"  => { quantity: 1 }
}, formulas: { quantity: :subtract })

Custom formulas are supported via a Proc that takes (lhs, rhs) or (lhs, rhs, model) and returns an Arel node:

add_tax = ->(lhs, rhs) { lhs + rhs + 1 }
Inventory.update_in_bulk(updates, formulas: { quantity: add_tax })

Examples

# Migration to combine two columns into one for all entries in a table.
Book.update_in_bulk([
  [{ written: false, published: false }, { status: :proposed }],
  [{ written: true,  published: false }, { status: :written }],
  [{ written: true,  published: true },  { status: :published }]
], record_timestamps: false)

# Relation scoping is preserved.
Employee.where(active: true).update_in_bulk({
  1 => { department: "Engineering" },
  2 => { department: "Sales" }
})

Restrictions

This method does not support relations with offset, limit, group, or having clauses. An order clause is supported by default by being stripped to keep the method usable on ordered associations.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/activerecord-updateinbulk/relation.rb', line 99

def update_in_bulk(updates, values = nil, record_timestamps: nil, formulas: nil)
  unless limit_value.nil? && offset_value.nil? && group_values.empty? && having_clause.empty?
    raise NotImplementedError, "No support to update relations with offset, limit, group, or having clauses"
  end
  if order_values.any? && !Builder.ignore_scope_order
    raise NotImplementedError, "No support to update ordered relations (order clause)"
  end

  conditions, assigns = Builder.normalize_updates(model, updates, values)
  return 0 if @none || conditions.empty?

  model.with_connection do |c|
    unless c.supports_values_tables?
      raise ArgumentError, "#{c.class} does not support VALUES table constructors"
    end

    arel = eager_loading? ? apply_join_dependency.arel : arel()
    arel.source.left = table
    arel.ast.orders = [] if Builder.ignore_scope_order

    values_table, conditions, set_assignments = Builder.new(
      self,
      c,
      conditions,
      assigns,
      record_timestamps:,
      formulas:
    ).build_arel
    if values_table
      arel = arel.join(values_table).on(*conditions)
    else
      conditions.each { |condition| arel.where(condition) }
    end

    key = if model.composite_primary_key?
      primary_key.map { |pk| table[pk] }
    else
      table[primary_key]
    end
    stmt = arel.compile_update(set_assignments, key)
    c.update(stmt, "#{model} Update in Bulk").tap { reset }
  end
end