Rails validates_uniqueness_of is completely broken
If you are using the Rails implementation of validates_uniqueness_of in your model to ensure duplicate data doesn’t get into your database, your application is broken. If you don’t use the database to ensure uniqueness with a key, then your app will fail at some point in the future. Probably just when you start getting some decent traffic and would like the app to not fail.
How do I know this? It happened to me this last weekend.
In checking this out, I find that Michael Koziarski of the Rails core team recently wrote
validates_uniqueness_of gives a nice error message, and does an ok job at guaranteeing uniqueness. Validates uniqueness of + a unique index does both.
In my app, does a nice job isn’t really good enough when it comes to uniqueness.
Michael has also posted that, hey, if you don’t like it, fix it and submit a patch, which is a great idea, but for this case is a really hard problem to solve for the general case across many databases.
I didn’t find a lot of specific fixes with code that I liked, so here’s how I fixed it in my app.
For my case, I already had a do some stuff and save the model method. In this method, I added some code (I stripped out my app specific code in the example).
DUPLICATE_ERROR_MESSAGES = [
"Duplicate entry"
]
def save_new
begin
save
rescue ActiveRecord::StatementInvalid => error
if DUPLICATE_ERROR_MESSAGES.any? { |msg| error.message =~ /#{Regexp.escape(msg)}/ }
logger.info "Duplicate Entry exception from DB"
errors.add_to_base('Duplicate item not allow')
return false
else
raise
end
end
endI also added a unique key to the table to ensure the MySQL database throws the exception that the above code relies on.
I still keep the validates_uniqueness_of call, as this does give a more specific error message, but now I don’t rely on it to enforce uniqueness.
Shh, don’t tell DHH that I’m putting integrity checks into my database.
Trackbacks
Use the following link to trackback from your own site:
http://localhost:5000/trackbacks?article_id=rails-validates_uniqueness_of-is-completely-broken&day=10&month=12&year=2007
I believe you could change
if DUPLICATEERRORMESSAGES.any? { |msg| error.message =~ /#{Regexp.escape(msg)}/ }
to
if error.message =~ Regexp.union(*DUPLICATEERRORMESSAGES)