Uniqueness Gotcha!!!

♦ The Problem

Consider the following relation where poll is having many options and each option of the poll must be having a unique description,

class Poll < ActiveRecord::Base
  has_many :options
  accepts_nested_attributes_for :options
end

class Option < ActiveRecord::Base
  belongs_to :poll
  validates_uniqueness_of :description, scope: :poll
end

Now, when trying to create a poll with its options using nested attributes, uniqueness validation is not getting applied due to race condition.

> poll = Poll.new(options_attributes: [ { description: 'test' }, { description: 'test' } ])
> poll.save
=> true
>
> poll.options
=> [#<Option id: 1, description: "test">, #<Option id: 2, description: "test">]

♦ Why it is occurring ?

There is a section in UniquenessValidator class, which mentions that the ActiveRecord::Validations#save does not guarantee the prevention of duplicates because, uniqueness checks are performed at application level which are prone to race condition when creating/updating records at the same time. Also, while saving the records, UniquenessValidator is checking uniqueness against the records which are in database only, but not for the records which are in memory.

♦ Is it a bug ?

Its kind of, because it allows creation of duplicate records and marks them as invalid after creation, not allowing to update them further.

> poll.options.last.valid?
=> false

♦ So, what are the solutions ?

As the doc mentions, you can add a unique index to that column, which guarantees record uniqueness at the database level and throws exception if there are any duplicates while creation/updation.

add_index :options, :description, unique: true
begin
  poll = Poll.new(options_attributes: [ { description: 'test' }, { description: 'test' } ])
  poll.save
rescue ActiveRecord::RecordNotUnique
  poll.errors.add(:options, 'must be unique')
end

Its important that you have to be careful to handle this exception every time when you are creating/updating the records(or you have to write a method which does this for you and calling it every time).

Another solution is, defining a custom validation in parent model which will check for child’s uniqueness,

class Poll < ActiveRecord::Base
  has_many :options
  accepts_nested_attributes_for :options

  validate :uniqueness_of_options

  private

  def uniqueness_of_options
    errors.add(:options, 'must be unique') if options.map(&:description).uniq.size != options.size
  end
end

Thanks for reading.

4 comments

  1. Gautam Rege · May 4, 2015

    Reblogged this on Josh Software – Where Programming is an Art! and commented:
    validation_uniqueness_of does not guarantee uniqueness. Interesting write-up by Yogesh about things to remember while working with uniqueness in Rails.

    Like

  2. Steven · May 5, 2015

    Right on. Another approach that will get less conflicts (but is also not guaranteed to provide uniqueness) is to use optimistic locking the rails way http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html

    Like

  3. deXterbed · September 12, 2015

    adding an index to avoid duplication is just wrong! better check the parameters for duplication or put a constraint at the DB level.

    Like

    • Yogesh Khater · October 16, 2015

      Adding unique index is same as adding a constraint at the DB level. Also, I think checking paramters might not provide a hard solution to avoid duplications in case of race conditions.

      Like

Leave a reply to deXterbed Cancel reply