Friday, April 1, 2016

Rails: Validate uniqueness of multiple columns

Leave a Comment

Is there a rails-way way to validate that an actual record is unique and not just a column? For example, a friendship model / table should not be able to have multiple identical records like:

user_id: 10 | friend_id: 20 user_id: 10 | friend_id: 20 

3 Answers

Answers 1

You can scope a validates_uniqueness_of call as follows.

validates_uniqueness_of :user_id, :scope => :friend_id 

Answers 2

You can use validates to validate uniqueness on one attribute:

validates :user_id, uniqueness: {scope: :friend_id} 

The syntax for the validation on multiple columns is similar, but you should provide an array of fields instead:

validates :attr, uniqueness: {scope: [:attr1, ... , :attrn]} 

However, approaches that are shown above suffer from race conditions, consider the following example:

  1. database table records are supposed to be unique by n fields;

  2. multiple (two or more) concurrent requests, handled by separate processes each (application server, sidekiq or whatever you are using), try to write the same record to the table;

  3. each process in parallel validates if there is a record with the same n fields;

  4. validation for each request is passed and each process creates a record in the table with the same data.

To avoid this kind of behaviour, one should add a unique constraint to the db table. You can set it with add_index for multiple (or one) fields by running the following migration:

class AddUniqueConstraints < ActiveRecord::Migration   def change    add_index :table_name, [:field1, ... , :fieldn], unique: true   end end 

Caveat : even after you've set the unique constraint, two or more concurrent requests will try to write the same data to the db, but instead of creating duplicate records, this will result in the raise of the ActiveRecord::RecordNotUnique exception, which you should handle separately:

begin # writing to the database rescue ActiveRecord::RecordNotUnique => e # handling the case when record already exists end  

Answers 3

You probably do need actual constraints on the db, because validates suffers from race conditions.

validates_uniqueness_of :user_id, :scope => :friend_id 

When you persist a user instance, Rails will validate your model by running a SELECT query to see if any user records already exist with the provided user_id. Assuming the record proves to be valid, Rails will run the INSERT statement to persist the user. This works great if you’re running a single instance of a single process/thread web server.

In case two processes/threads are trying to create a user with the same user_id around the same time, the following situation may arise. Race condition with validates

With unique indexes on the db in place, the above situation will play out as follows. Unique indexes on db

Answer taken from this blog post - http://robots.thoughtbot.com/the-perils-of-uniqueness-validations

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment