There are some use cases where optimistic locking is useful, but doesn’t need the full complexity of a lock_version
increment. For example, say a record in the database needs to be “claimed” by the first user who claims it.
| id | user_id |
|----|---------|
| 1 | NULL |
If I have 2 requests in parallel,
- Request 1:
Record.find(1).update(user_id: 111)
- Request 2:
Record.find(1).update(user_id: 222)
And if both objects are instantiated while user_id
is still NULL
, then last one will always “win”, even if user_id was no longer null at the moment of commit.
So even if first one wrote 111
, the second would change it to 222
.
I could use either pessimistic or optimistic locking for this. In my case optimistic makes more sense from performance standpoint. However, lock_version
seems like an overkill, since all I’m really doing is:
UPDATE records SET user_id = 222 WHERE id = 1 AND user_id is NULL
Then raising an error if affected_rows.zero?
.
I could technically do this using Record.update_all
, but then I’m losing all the callbacks/validations/etc.
Proposal
What if we had something like update_if
or save_if
function that accepted the constraints hash?
account.update_if({balance: 400}, balance: 500)
and/or
account.balance = 500
account.save_if(balance: 400)
or
account.update(balance: 500).where(balance: 400)
This would support all the callback/validation stuff rails has, maybe even accept standard save **options
, but also raise ActiveRecord::StaleObjectError
if original balance wasn’t 400.
The exact interface of this feature is definitely up for debate. What do ya’ll think? Or is there something obvious I’m missing that does this already, while preserving callbacks?
P.S. And the existing lock_version
code could then be rewritten on top of this feature, since the behavior is largely the same.