Rails and validates_uniqueness_of
I’m a Rails dabbler who has never actually deployed an application yet but has been working in spare moments on a new version of this site that will be powered by Rails.
I was puzzled today to see excess SELECT
queries whenever I saved a record. The queries were for attributes that were entirely unrelated to what I was manipulating.
I’ll spare you the long story and cut to the chase. It turns out that these extra queries are generated by validates_uniqueness_of
. I guess I should have known. The entire model gets validated whenever you save, even attributes that you haven’t touched. I guess this makes sense.
Now, I am not really sure about the point of validates_uniqueness_of
because it is vulnerable to a classic race condition; basically Rails checks "is this unique?" and if it is goes ahead and saves the record. Of course, in a multi-user application it’s possible that what was unique a millisecond again is no longer so.
Basically validates_uniqueness_of
can’t provide you with any guarantees, and if that’s the case then why bother using it? I had it in my app mostly for experimentation purposes.
The only robust way to approach this is to do what we always do when we have race conditions to worry about: go ahead and save the record and only if it doesn’t work worry about what you’ll do. In order for this to work you need to impose a database-level constraint which will cause an exception to be thrown if someone tries to create a duplicate (and of course, the database won’t permit the creation of the duplicate to succeed).
Something like this in a migration should do it:
add_index :things, :attribute, :unique => true
Try to insert a "thing" with a duplicate "attribute" and MySQL will complain about a "Duplicate entry" and Rails will duly raise an ActiveRecord::ActiveRecordError
which you can rescue and deal with appropriately.
If you’re doing that then is there any point in keeping those validates_uniqueness_of
calls? Perhaps. I guess if the probability of races is low then you can keep your application logic simpler by relying on validates_uniqueness_of
for informing the user nicely that "that name is already taken", and rely on database-level constraints to handle the really nasty race conditions (and throw an irrecoverable error). I suppose it depends on how much traffic and contention you’re expecting. If traffic is high and performance is a real issue then you might just want to get rid of the validation; it doesn’t provide any guarantees and those extra queries will only slow you down.