Thursday, November 29, 2007

validates_uniqueness_of is broken, and dangerous

Update: Progess. The Rails API docs now mention this problem. Thanks, Koz.


validates_uniqueness_of guarantees unique column values, just like a unique key constraint in a database, right? Wrong. It doesn't guarantee anything.

Suppose your model validates_uniqueness_of :user_name. Two requests come along, each in a separate process. Both try to save a new row with a user_name of 'foobar'.
  • Request 1 checks 'foobar' - no problem.

  • Request 2 checks 'foobar' - no problem.

  • Request 1 inserts.

  • Request 2 inserts.
Oops - duplicate user_name. If you want more technical detail, here's a good link.

This is a nasty, subtle bug. You won't see it in your unit tests. You probably won't see it under low loads. Depending on your application's design, you may never see it.

But, if you do see it, you'll see it under high request volumes. Once in a blue moon, maybe. You'll see it and tell yourself it's impossible, because you're validating uniqueness. And you may spend a lot of time looking for the answer, because the problem is not well known. The Rails API documentation doesn't mention it. The Agile book mentions it, but way down on page 371, well after validates_uniqueness_of has been introduced.

Why don't "they" fix it? Maybe because making it work correctly, and efficiently, across multiple database platforms, is far from trivial. But "they" should document the problem.

Should you stop using validates_uniqueness_of? Not necessarily. It'll do the right thing almost all the time, the error messages are handy, and it's a good way to show your intentions in your model.

But always back it up with constraints in the database. When that blue moon rises, better to have a save fail with a clumsy error message than store duplicate values in a column that should be unique.

6 comments:

  1. You could try and solve this problem within <insert environment of choice>, however I think you're wasting your time. Database vendors have worked on this problem for years and it is one of their primary goals. To think that someone could hash out a proper solution to this problem within Rails is a little naive.

    In my opinion, the developer needs to be aware that checking if a username exists before doing an insert may still fail if another user has beaten them to it. This isn't a problem to be honest, you just need to be aware of it and deal with the resulting unique key violation appropriately.

    What do you propose the developers do to fix this?

    ReplyDelete
  2. I just blogged on this subject too. My blog post has some code for how I handle this case. My fix is MySQL specific and doesn't show the name of the column that failed, but it does set a reasonable error text that can be displayed to the user.

    http://blog.craz8.com/articles/2007/12/10/rails-validates_uniqueness_of-is-completely-broken/

    ReplyDelete
  3. "Database vendors have worked on this problem for years..."
    Isn't that what database transactions are for?

    ReplyDelete
  4. Hello ? validates_uniqueness operates in an atomic transaction. That's what BEGIN and COMMIT are for.

    ReplyDelete
  5. My solution: https://rails.lighthouseapp.com/projects/8994/tickets/3486-alternative-to-validates_uniqueness_of-using-db-constraints Potential upsides/downsides (that I could think of) enumerated in the ticket. Feedback/suggestions always welcome.

    ReplyDelete

Note: Only a member of this blog may post a comment.