Rails ActiveRecord - vaildate uniqueness with named scope (not the scope like other columns)

Viewed 94

Imagine a Users table:

id email access_token refresh_token country_id city_id discarded_at
1 james.dean@example.com 11111 22222 1 1 2021-01-31 00:57:25
2 james.bond@example.com 33333 44444 1 3 NULL
3 james.franco@example.com 55555 66666 2 4 NULL
4 james.corden@example.com 77777 88888 1 5 NULL
5 james.dean@example.com 11111 22222 1 1 NULL

Usually, validating uniqueness of other columns is like this

validates :emails, :access_token, :refresh_token, uniqueness: { scope: %i[country_id city_id] }

Now the question is - how to do it for a named scope?

Say for example, there's a named scope scope :kept, -> { where(discarded_at: nil) }, then email, access_token and refresh_token should be unique for all the kept users.

(With the example above, i.e. ID=5 is valid because ID=1 is already discarded, so email, access_token and refresh_token of ID=5 are valid)

class User < ApplicationRecord
  scope :kept, -> { where(discarded_at: nil) }

   # maybe something like this?
  validates :email, :access_token, :refresh_token, uniqueness: { where(discarded_at: nil) }
end
2 Answers

You can use the conditions option (as outlined here)

validates_uniqueness_of :email, :access_token, :refresh_token, conditions: -> { where(discarded_at: nil) }

Just to further expand @AbM's answer together with @r4cc00n's comment.

Here's the complete answer:

  • with the new validates syntax
  • it needs to allow_nil
  • add DB level index

Model

validates :email, :access_token, :refresh_token,
          uniqueness: { allow_nil: true, conditions: -> { where(discarded_at: nil) } }

Migration

class CreateUniqueIndiceToUsers < ActiveRecord::Migration[6.0]
  def change
    add_index :users, :email, unique: true, where: "(discarded_at IS NULL)"
    add_index :users, :access_token, unique: true, where: "(discarded_at IS NULL)"
    add_index :users, :refresh_token, unique: true, where: "(discarded_at IS NULL)"
  end
end
Related