Boring Rails

Tip: Rails validations: unique within a certain scope

:fire: Tiny Tips

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

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

If you like these tips, you'll love my Twitter account. All killer, no filler.