require 'active_support/core_ext/array/prepend_and_append' module ActiveRecord module Validations class UniquenessValidator < ActiveModel::EachValidator def initialize(options) super(options.reverse_merge(:case_sensitive => true)) end # Unfortunately, we have to tie Uniqueness validators to a class. def setup(klass) @klass = klass end def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) table = finder_class.arel_table coder = record.class.serialized_attributes[attribute.to_s] if value && coder value = coder.dump value end relation = build_relation(finder_class, table, attribute, value) relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted? Array(options[:scope]).each do |scope_item| scope_value = record.send(scope_item) reflection = record.class.reflect_on_association(scope_item) if reflection scope_value = record.send(reflection.foreign_key) scope_item = reflection.foreign_key end relation = relation.and(table[scope_item].eq(scope_value)) end relation = finder_class.unscoped.where(relation) if options[:conditions] relation = relation.merge(options[:conditions]) end if relation.exists? record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope, :conditions).merge(:value => value)) end end protected # The check for an existing value should be run from a class that # isn't abstract. This means working down from the current class # (self), to the first non-abstract class. Since classes don't know # their subclasses, we have to build the hierarchy between self and # the record's class. def find_finder_class_for(record) #:nodoc: class_hierarchy = [record.class] while class_hierarchy.first != @klass class_hierarchy.prepend(class_hierarchy.first.superclass) end class_hierarchy.detect { |klass| !klass.abstract_class? } end def build_relation(klass, table, attribute, value) #:nodoc: reflection = klass.reflect_on_association(attribute) if reflection column = klass.columns_hash[reflection.foreign_key] attribute = reflection.foreign_key value = value.attributes[reflection.primary_key_column.name] else column = klass.columns_hash[attribute.to_s] end value = column.limit ? value.to_s[0, column.limit] : value.to_s if !value.nil? && column.text? if !options[:case_sensitive] && value && column.text? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation relation = klass.connection.case_insensitive_comparison(table, attribute, column, value) else value = klass.connection.case_sensitive_modifier(value) unless value.nil? relation = table[attribute].eq(value) end relation end end module ClassMethods # Validates whether the value of the specified attributes are unique across the system. # Useful for making sure that only one user # can be named "davidhh". # # class Person < ActiveRecord::Base # validates_uniqueness_of :user_name # end # # It can also validate whether the value of the specified attributes are unique based on a scope parameter: # # class Person < ActiveRecord::Base # validates_uniqueness_of :user_name, :scope => :account_id # end # # Or even multiple scope parameters. For example, making sure that a teacher can only be on the schedule once # per semester for a particular class. # # class TeacherSchedule < ActiveRecord::Base # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] # end # # It is also possible to limit the uniqueness constraint to a set of records matching certain conditions. # In this example archived articles are not being taken into consideration when validating uniqueness # of the title attribute: # # class Article < ActiveRecord::Base # validates_uniqueness_of :title, :conditions => where('status != ?', 'archived') # end # # When the record is created, a check is performed to make sure that no record exists in the database # with the given value for the specified attribute (that maps to a column). When the record is updated, # the same check is made but disregarding the record itself. # # Configuration options: # * :message - Specifies a custom error message (default is: "has already been taken"). # * :scope - One or more columns by which to limit the scope of the uniqueness constraint. # * :conditions - Specify the conditions to be included as a WHERE SQL fragment to limit # the uniqueness constraint lookup. (e.g. :conditions => where('status = ?', 'active')) # * :case_sensitive - Looks for an exact match. Ignored by non-text columns (+true+ by default). # * :allow_nil - If set to true, skips this validation if the attribute is +nil+ (default is +false+). # * :allow_blank - If set to true, skips this validation if the attribute is blank (default is +false+). # * :if - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). # The method, proc or string should return or evaluate to a true or false value. # * :unless - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. :unless => :skip_validation, or # :unless => Proc.new { |user| user.signup_step <= 2 }). The method, proc or string should # return or evaluate to a true or false value. # # === Concurrency and integrity # # Using this validation method in conjunction with ActiveRecord::Base#save # does not guarantee the absence of duplicate record insertions, because # uniqueness checks on the application level are inherently prone to race # conditions. For example, suppose that two users try to post a Comment at # the same time, and a Comment's title must be unique. At the database-level, # the actions performed by these users could be interleaved in the following manner: # # User 1 | User 2 # ------------------------------------+-------------------------------------- # # User 1 checks whether there's | # # already a comment with the title | # # 'My Post'. This is not the case. | # SELECT * FROM comments | # WHERE title = 'My Post' | # | # | # User 2 does the same thing and also # | # infers that his title is unique. # | SELECT * FROM comments # | WHERE title = 'My Post' # | # # User 1 inserts his comment. | # INSERT INTO comments | # (title, content) VALUES | # ('My Post', 'hi!') | # | # | # User 2 does the same thing. # | INSERT INTO comments # | (title, content) VALUES # | ('My Post', 'hello!') # | # | # ^^^^^^ # | # Boom! We now have a duplicate # | # title! # # This could even happen if you use transactions with the 'serializable' # isolation level. The best way to work around this problem is to add a unique # index to the database table using # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the # rare case that a race condition occurs, the database will guarantee # the field's uniqueness. # # When the database catches such a duplicate insertion, # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid # exception. You can either choose to let this error propagate (which # will result in the default Rails exception page being shown), or you # can catch it and restart the transaction (e.g. by telling the user # that the title already exists, and asking him to re-enter the title). # This technique is also known as optimistic concurrency control: # http://en.wikipedia.org/wiki/Optimistic_concurrency_control # # The bundled ActiveRecord::ConnectionAdapters distinguish unique index # constraint errors from other types of database errors by throwing an # ActiveRecord::RecordNotUnique exception. # For other adapters you will have to parse the (database-specific) exception # message to detect such a case. # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception: # * ActiveRecord::ConnectionAdapters::MysqlAdapter # * ActiveRecord::ConnectionAdapters::Mysql2Adapter # * ActiveRecord::ConnectionAdapters::SQLiteAdapter # * ActiveRecord::ConnectionAdapters::SQLite3Adapter # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter # def validates_uniqueness_of(*attr_names) validates_with UniquenessValidator, _merge_attributes(attr_names) end end end end