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.