Updates ActiveRecord records only when they match a condition in the database.
The condition is part of the UPDATE statement itself, so it can't change between checking it and updating the record (eg. by a concurrent update).
Add this line to your application's Gemfile:
gem 'active_record-update_when'And then execute:
$ bundleThen include ActiveRecord::UpdateWhen in your models (typically in ApplicationRecord, so all models get it):
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
include ActiveRecord::UpdateWhen
endupdate_when updates a record (as update would) if it matches the condition in the database:
order = Order.find(...)
if order.update_when(condition: Order.checked_out, attributes: { status: 'cancelled' })
# order was cancelled
elsif order.errors.any?
# order was invalid
else
# order wasn't checked-out
endUPDATE "orders" SET "status" = 'cancelled', "updated_at" = ...
WHERE "orders"."id" = 1 AND "orders"."status" = 'checked_out'It returns false if the record is invalid (with errors), or if it doesn't match the condition (without errors). When the condition isn't met, the update is rolled back, including any changes made by its callbacks (even within another transaction).
update_when! updates the record as update! would. It returns false if the record doesn't match the condition,
and raises whenever update! would (eg. ActiveRecord::RecordInvalid):
order.update_when!(condition: Order.checked_out, attributes: { status: 'cancelled' })The condition is a relation of the record's model. Its WHERE clause is added to the UPDATE:
order.update_when(condition: Order.where(status: %w[pending checked_out]), attributes: { status: 'cancelled' })Conditions can also have joins, GROUP BY and HAVING clauses (including for models with composite primary keys), in which case the record is matched by an EXISTS subquery:
order.update_when(condition: Order.checked_out.joins(:lines), attributes: { status: 'cancelled' })If the attributes don't change the record, nothing is updated (as with update) and the condition isn't checked.
The specs run against a dummy Rails application in spec/dummy, using SQLite by default:
$ bin/rspec
$ bin/rubocopPostgreSQL and MySQL are selected with the DATABASE_ADAPTER and DATABASE_URL environment variables
(see .github/workflows/ci.yml).
The gem is available as open source under the terms of the MIT License.