Rails validates_uniqueness_of is completely broken

December 10, 2007 Link to post  Permalink

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).

 2      "Duplicate entry"
 3  ]
 5  def save_new
 6    begin
 7      save    
 8    rescue ActiveRecord::StatementInvalid => error
 9      if DUPLICATE_ERROR_MESSAGES.any? { |msg| error.message =~ /#{Regexp.escape(msg)}/ }
10        logger.info "Duplicate Entry exception from DB"
11        errors.add_to_base('Duplicate item not allow')
12        return false
13      else
14        raise
15      end
16    end
17  end

I 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.