Tip: Rails validations: unique within a certain scope
It’s a great idea to make your database and application validations match. If you have validates :name, presence: true
in your model, you should pair it with a not null
database constraint. Unique validations should be paired with a UNIQUE
database index.
In real-world applications, you often have more complicated validations, but you should continue this practice whenever you can.
Something I encounter regularly is the need to have records that are unique, but within a certain scope.
Imagine you were building a typical project management tool. You might want Project
s to have a unique name so they can be distinguished within your UI – but you don’t want the name to be globally unique. If I make a project called “Onboarding”, another customer should not be restricted from using that name as well.
Luckily, Rails has got us covered with a handy feature called validation scopes.
Usage
The scope
option to the Rails uniqueness validation rule allows us to specify additional columns to consider when checking for uniqueness.
class Project < ApplicationRecord
belongs_to :account
has_many :tasks
validates :name, presence: true, uniqueness: { scope: :account_id }
end
This rule says that “the name of this project must unique, within the scope of this account”. In other words, the combination of a name
and account_id
must be unique – but you can have projects with the same name in different accounts.
As we discussed earlier, you really want to back-up your application level validations with database constraints.
In this case, you’ll want to do a multiple column index. You can do this in a normal Rails migration.
class CreateProject < ActiveRecord::Migration[6.0]
def change
create_table :projects do |t|
...
end
add_index :projects, [:name, :account_id], unique: true
end
end
Options
You can pass multiple columns to scope
.
If you were building a dining app and wanted to enforce that a guest could only have one reservation at a restaurant per day.
class Reservation < ApplicationRecord
belongs_to :guest
belongs_to :restaurant
validates :guest_id, uniqueness: {
scope: [ :restaurant_id, :reservation_date ]
}
end
You may wish to change the message since the defaults error message will be fairly spartan: “{field} has already been taken”
validates :guest_id, uniqueness: {
scope: [ :restaurant_id, :reservation_date ],
message: "Only one reservation per guest per day is permitted"
}
Note: In PostgreSQL, the default limit for index names is 63 characters so you may find yourself needing to change the index name if your model or column names are longer.
add_index :reservations, [:guest_id, :restaurant_id, :reservation_date],
unique: true,
name: "idx_reserveration_guest_date_uniq"
Additional Resources
Rails API: Uniqueness Validations
PostgreSQL Docs: Postgres Constraints
MySql Docs: Multi-column Indexes