Real validates_uniqueness_of in Rails

[Posted by Urban Hafner on 19 Aug 2009]

We all know that validates_uniqueness_of doesn’t provide a real guarantee for unique database entries. For most cases though it’s good enough. But there are cases where you either get so much traffic that this problem becomes an issue, or it’s absolutely critical that the values in the database are unique.

If this is the case there’s no way this can be done entirely in Rails. After all, how should Rails know that the values in the database are unique? That’s the job of the database. So we have to go that route. In my case I had to generate a unique token for a user. The code started out as:

class User < ActiveRecord::Base

  validates_uniqueness_of :token
  before_validation_on_create :generate_token
  
  private
  
  def generate_token
    self.token = rand(36**10).to_s(36)
  end
end

In this case I might run into the above mentioned race condition. Luckily though I can also just retry saving the record and (hopefully) will end up with a unique token if the saving fails the first time.

So first, we’ll need to ensure that the database keeps the entries in the token column of the user table unique. To ensure this, we’ll add an unique index on this field:

class AddUniqueTokenIndexToUser < ActiveRecord::Migration
  def self.up
    add_index :users, :token, :unique => true
  end

  def self.down
    remove_index :users, :token
  end
end

Now, whenever Rails tries to save an row into the database that contains a duplicate entry for the token column a ActiveRecord::StatementInvalid exception will be thrown. All though that’s not ideal (as this exception might have an entirely different cause) we can catch that exception and try saving again.

We’d have to that that in every place where we want to create/modify a User though. So wouldn’t it be much nicer if save (and save!, …) would just do that for us? Indeed it would be nice so let’s get our meta programming toolkit out and write the code for that:

# Use "extend DbUnique" to get this functionality
module DbUnique
  
  DUPLICATE_ERROR_MESSAGES = [
    "Duplicate entry",           # MySQL
    /column (\w+) is not unique/ # SQLite
  ]
  
  def db_unique(*methods)
    methods.each do |method|
      module_eval <<-"end;"
        alias old_#{method} #{method}
        def #{method}(*args)
          retries = 0
          begin
            old_#{method}(*args)
          rescue ActiveRecord::StatementInvalid => error
            if error.message =~ Regexp.union(*DUPLICATE_ERROR_MESSAGES)
              retries += 1
              if retries < 5
                retry
              else
                raise
              end
            else
              raise
            end
          end
        end
      end;
    end
  end
  
end

We can then just include that code into our ActiveRecord model and overwrite the methods we want with versions that retry writing to the database a number of times:

class User < ActiveRecord::Base

  extend DbUnique  
  db_unique :save, :save!
  
end

Tags ruby, rails, validates_uniqueness_of, unique, uniqueness

blog comments powered by Disqus