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:
database table records are supposed to be unique by n fields;
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;
each process in parallel validates if there is a record with the same n fields;
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.
With unique indexes on the db in place, the above situation will play out as follows.
Answer taken from this blog post - http://robots.thoughtbot.com/the-perils-of-uniqueness-validations
0 comments:
Post a Comment