206

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
potashin
  • 44,205
  • 11
  • 83
  • 107
re5et
  • 4,305
  • 3
  • 25
  • 26
  • 2
    try using "validates_uniqueness_of" in your model. if this doesnt work the try to create an index on which you can create a migration of feilds which includes a statement like add_index :table, [:column_a, :column_b], :unique => true) – Harry Joy Feb 02 '11 at 05:38
  • 2
    Unfortunately `validates :field_name, unique: true` is prone to race conditions, so even though against rails-way, an actual constraint is prefered. @HarryJoy I'll upvote an answer describing constraint way. – Pooyan Khosravi May 13 '14 at 13:43
  • @Green To be fair, that's an excellent way of ensuring that it will NEVER happen. Validations can be bypassed. – Frans Aug 19 '14 at 07:55
  • 2
    better answer then all the noted below is this one http://stackoverflow.com/a/34425284/1612469 as it brings another layer for making sure everything will work correctly – Aleks Jun 08 '16 at 11:42

3 Answers3

324

You can scope a validates_uniqueness_of call as follows.

validates_uniqueness_of :user_id, :scope => :friend_id
potashin
  • 44,205
  • 11
  • 83
  • 107
Dylan Markow
  • 123,080
  • 26
  • 284
  • 201
  • 84
    Just wanted to add that you can pass multiple scope params in case you need to validate uniqueness on more than 2 fields. I.e. :scope => [:friend_id, :group_id] – Dave Rapin May 02 '11 at 16:36
  • 27
    Weird that you cannot say `validates_uniqueness_of [:user_id, :friend_id]`. Maybe this needs to be patched? – Alexey Jul 16 '12 at 20:09
  • 14
    Alexey, validates_uniqueness_of [:user_id, :friend_id] will just do the validation for each of fields listed - and it is documented and expected behavior – Nikita Hismatov Mar 18 '13 at 09:52
  • 72
    In Rails 4, this becomes: validates :user_id, uniqueness: {scope: :friend_id} – Marina Martin Jan 26 '14 at 18:36
  • 3
    You probably want to add a custom error msg like , :message => ' has already this friend.' – laffuste May 15 '14 at 09:34
  • 2
    a unique SQL index should also be used as there is too much magic going on in rails' validators to rely them solely IMO, e.g. try validating a boolean value – engineerDave Mar 30 '16 at 17:00
  • @DaveRapin for the sake of new comers, what do you think of adding a migration adding an index to your answer, along the lines of the one [here](https://stackoverflow.com/questions/34424154/rails-validate-uniqueness-of-two-columns-together/34425284#34425284) (most programmers will recognise that race conditions could still be a risk here, but many may not) – stevec Sep 17 '20 at 08:59
157

You can use validates to validate uniqueness by one column:

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

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

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

However, validation approaches shown above have a race condition and can’t ensure consistency. 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 servers, background worker servers or whatever one is using), access database to insert the same record in table;

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

  4. validation for each request is passed successfully, 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 db table. You can set it with add_index helper for one (or multiple) field(s) 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 a unique constraint, two or more concurrent requests will try to write the same data to db, but instead of creating duplicate records, this will raise an ActiveRecord::RecordNotUnique exception, which you should handle separately:

begin
# writing to database
rescue ActiveRecord::RecordNotUnique => e
# handling the case when record already exists
end 
potashin
  • 44,205
  • 11
  • 83
  • 107
3

This can be done with a database constraint on the two columns:

add_index :friendships, [:user_id, :friend_id], unique: true

You could use a rails validator, but in general I recommend using a database constraint.

More reading: https://robots.thoughtbot.com/validation-database-constraint-or-both

Tate Thurston
  • 4,236
  • 1
  • 26
  • 22