DEV Community

matt swanson
matt swanson

Posted on • Originally published at boringrails.com on

5 3

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 Projects 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Additional Resources

Rails API: Uniqueness Validations

PostgreSQL Docs: Postgres Constraints

MySql Docs: Multi-column Indexes


Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

AWS Q Developer image

Your AI Code Assistant

Generate and update README files, create data-flow diagrams, and keep your project fully documented. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE