ActiveRecord: ensure only one record has specific attribute value?
I have a Races table with many races in various states. But I need to ensure that only one race is marked as current = true. Here is what I have been using in the Race model validation.
# current: boolean
validate :only_one_current
private
def only_one_current
if self.current && (Race.current_race.id != self.id)
errors.add(:base, "Races can have only one current race")
开发者_Go百科 end
end
This seems to work most of the time, but occasionally it does not and I'm not sure why. When it doesn't work it disallows the saving of a new record with current = t just after a different record that was current is deleted. I think it has to do with AR's persistence.
There must be a better way to do this?
Your problem actually extends beyond ActiveRecord. No matter how you implement your before_save method, it will always be possible for a race condition to occur (no pun intended), and for two records to have current = true in the database. See the Concurrancy and Integrity section for validates_uniqueness_of for more information.
The core problem is that the logic to check whether a record has current = true and the operation set the record to current = true is not atomic. This issue comes up in concurrent systems often.
To solve this, you need a unique key index in the database. I'd recommend that you change your current flag to a priority field. The priority is an integer which has a unique key index. The database will guarantee that no two records exist at the same time with the same priority value. The "current" race will always be the one that has the highest priority value.
The race condition will actually still exist - you now just have a way of detecting it. When you set a race to current (by querying the table for the largest priority value), an exception will be generated if another record currently holds the same priority value as the one you're trying to save. Simply catch the duplicate key exception and try again.
You need to call this as before_save, not as a validator:
before_save :only_one_current
精彩评论