Guaranteeing uniqueness and persistence with ActiveRecord

Generally, when someone wants to ensure that a models field is unique, they use a database unique index in conjunction with validates_uniqueness_of :field.
The problem with this approach is that while the validation rule will hit the database checking if another record with the given value for the given column exists, and if not, it tries to save the record.

So why won’t it work?

As documented in the Rails source, this approach is error prone, especially for race-conditions (see here for more information on this).

Basically what happens is that record one checks if the field value “John” for the field :name already exists in the table, if not, it calls save. Now a second record with the same value “John” is created at the same time, which also concludes that there is no existing record with that value in the database, and proceeds to save.

As both record pass validations and call save, one of them actually gets saved, and the other one gets blown off with a icky ActiveRecord::StatementInvalid exception due to the database unique index constraints being violated.

Isn’t this acceptable?

In most cases – yes. But I have an Order object that is lazily initialized i.e the user clicks “Add to cart”, applications checks if a record for this user already exists, if not it creates one and proceeds to adding stuff into the cart and displaying the shopping cart page. And every order must get an unique token during the creation process.

It would be unacceptable to display an error in case of a race condition to the user during this process. I need to have a guarantee that the Order gets persisted to the database with an unique token.

But it can’t be guaranteed!

Technically no. But I can get close enough. I just need to recover from the unique database constraint error and retry with another value.

To avoid the database being locked up by several processes trying and retrying to generate and save new record with unique tokens (and possibly failing due to some other error instead of the unique index one), i define a reasonable retry count.

Then I can assume that if the record could not be saved within that number of retries, it’s either broken for some other reason (or we’re dealing with an unusually high amount of token uniqueness clashes).

class Order > ActiveRercord::Base

  after_create :set_token

  protected

  def set_token
    begin
      self.transaction do
        update_attribute(:token, Digest::MD5.hexdigest(Array.new(10){rand(9)}.join))
      end
    rescue ActiveRecord::StatementInvalid => error
      @token_retry_count = (@token_retry_count || 0) + 1

      if @token_retry_count < 10
        set_order_token
      else
        raise error
      end

    end
  end

end

Seems fragile…

But it works. Unless you reach the limit of unique combinations for your unique field. That’s why the exception is re-raised on unsuccessful retries – you can crawl the logs and react when something seems really broken. Until then it’s good enough, and most importantly it’s much more reliable than the validates_uniqueness_of :token, providing a better user experience.

Also, if you have any suggestions or questions, or great ideas how to achieve guaranteed uniqueness with error recovery and (almost) guaranteed persistence, do not hesitate to leave a comment.

Tanel Suurhans
Tanel is an experienced Software Engineer with strong background in variety of technologies. He is extremely passionate about creating high quality software and constantly explores new technologies.

1 Comment

    Liked this post?

    There’s more where that came from. Follow us on Facebook, Twitter or subscribe to our RSS feed to get all the latest posts immediately.