Module: ActiveRecord::UpdateInBulk::Relation
- Included in:
- Relation
- Defined in:
- lib/activerecord-updateinbulk/relation.rb
Instance Method Summary collapse
-
#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
UPDATEstatement.
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_timestampsconfig, 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: :alwaysto 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 |