uniqueness.rb 10.9 KB
Newer Older
1
module ActiveRecord
2
  module Validations
3
    class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
4
      def initialize(options)
5 6
        if options[:conditions] && !options[:conditions].respond_to?(:call)
          raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \
7
                               "Pass a callable instead: `conditions: -> { where(approved: true) }`"
8
        end
9
        super({ case_sensitive: true }.merge!(options))
10
        @klass = options[:class]
11 12
      end

13
      def validate_each(record, attribute, value)
14
        return unless should_validate?(record)
15
        finder_class = find_finder_class_for(record)
16
        table = finder_class.arel_table
R
Rafael Mendonça França 已提交
17
        value = map_enum_attribute(finder_class, attribute, value)
18

19
        relation = build_relation(finder_class, table, attribute, value)
20
        relation = relation.where.not(finder_class.primary_key => record.id) if record.persisted?
C
Carlos Antonio da Silva 已提交
21
        relation = scope_relation(record, table, relation)
22
        relation = relation.merge(options[:conditions]) if options[:conditions]
23 24

        if relation.exists?
25 26 27 28
          error_options = options.except(:case_sensitive, :scope, :conditions)
          error_options[:value] = value

          record.errors.add(attribute, :taken, error_options)
29 30 31 32 33 34 35 36 37 38 39 40 41
        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
42
          class_hierarchy.unshift(class_hierarchy.first.superclass)
43 44 45 46 47
        end

        class_hierarchy.detect { |klass| !klass.abstract_class? }
      end

48
      def build_relation(klass, table, attribute, value) #:nodoc:
49
        if reflection = klass._reflect_on_association(attribute)
50
          attribute = reflection.foreign_key
51
          value = value.attributes[reflection.klass.primary_key] unless value.nil?
52
        end
53

54 55 56 57 58 59 60 61 62
        attribute_name = attribute.to_s

        # the attribute may be an aliased attribute
        if klass.attribute_aliases[attribute_name]
          attribute = klass.attribute_aliases[attribute_name]
          attribute_name = attribute.to_s
        end

        column = klass.columns_hash[attribute_name]
63 64
        cast_type = klass.type_for_attribute(attribute_name)
        value = cast_type.type_cast_for_database(value)
65
        value = klass.connection.type_cast(value)
66 67 68
        if value.is_a?(String) && column.limit
          value = value.to_s[0, column.limit]
        end
69

70
        value = Arel::Nodes::Quoted.new(value)
71

72
        comparison = if !options[:case_sensitive] && value && cast_type.text?
73
          # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
74
          klass.connection.case_insensitive_comparison(table, attribute, column, value)
75
        else
76
          klass.connection.case_sensitive_comparison(table, attribute, column, value)
77
        end
78
        klass.unscoped.where(comparison)
79
      end
C
Carlos Antonio da Silva 已提交
80 81 82

      def scope_relation(record, table, relation)
        Array(options[:scope]).each do |scope_item|
83
          if reflection = record.class._reflect_on_association(scope_item)
C
Carlos Antonio da Silva 已提交
84 85 86
            scope_value = record.send(reflection.foreign_key)
            scope_item  = reflection.foreign_key
          else
87
            scope_value = record._read_attribute(scope_item)
C
Carlos Antonio da Silva 已提交
88
          end
89
          relation = relation.where(scope_item => scope_value)
C
Carlos Antonio da Silva 已提交
90 91 92 93 94
        end

        relation
      end

R
Rafael Mendonça França 已提交
95
      def map_enum_attribute(klass, attribute, value)
96
        mapping = klass.defined_enums[attribute.to_s]
97 98
        value = mapping[value] if value && mapping
        value
C
Carlos Antonio da Silva 已提交
99
      end
100 101
    end

102
    module ClassMethods
103 104
      # Validates whether the value of the specified attributes are unique
      # across the system. Useful for making sure that only one user
105 106 107
      # can be named "davidhh".
      #
      #   class Person < ActiveRecord::Base
108
      #     validates_uniqueness_of :user_name
109 110
      #   end
      #
111 112
      # It can also validate whether the value of the specified attributes are
      # unique based on a <tt>:scope</tt> parameter:
113 114
      #
      #   class Person < ActiveRecord::Base
115
      #     validates_uniqueness_of :user_name, scope: :account_id
116
      #   end
117
      #
118 119 120
      # 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.
121 122
      #
      #   class TeacherSchedule < ActiveRecord::Base
123
      #     validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
124 125
      #   end
      #
126 127 128
      # 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
129 130 131
      # of the title attribute:
      #
      #   class Article < ActiveRecord::Base
132
      #     validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') }
133 134
      #   end
      #
135 136 137
      # 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,
138
      # the same check is made but disregarding the record itself.
139 140
      #
      # Configuration options:
141 142 143 144 145 146 147
      #
      # * <tt>:message</tt> - Specifies a custom error message (default is:
      #   "has already been taken").
      # * <tt>:scope</tt> - One or more columns by which to limit the scope of
      #   the uniqueness constraint.
      # * <tt>:conditions</tt> - Specify the conditions to be included as a
      #   <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup
148
      #   (e.g. <tt>conditions: -> { where(status: 'active') }</tt>).
149 150 151 152 153 154 155 156 157 158 159
      # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
      #   non-text columns (+true+ by default).
      # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
      #   attribute is +nil+ (default is +false+).
      # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the
      #   attribute is blank (default is +false+).
      # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
      #   if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
      #   or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
      #   proc or string should return or evaluate to a +true+ or +false+ value.
      # * <tt>:unless</tt> - Specifies a method, proc or string to call to
160
      #   determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
161 162 163
      #   or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
      #   method, proc or string should return or evaluate to a +true+ or +false+
      #   value.
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
      #
      # === 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
183
      #                                      | # infers that their title is unique.
184 185 186
      #                                      | SELECT * FROM comments
      #                                      | WHERE title = 'My Post'
      #                                      |
187
      #  # User 1 inserts their comment.     |
188 189 190 191 192 193 194 195 196 197 198 199 200 201
      #  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'
202 203 204 205 206
      # 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.
207
      #
208 209 210 211 212
      # 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
213
      # that the title already exists, and asking them to re-enter the title).
214 215
      # This technique is also known as
      # {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control].
216
      #
217 218
      # The bundled ActiveRecord::ConnectionAdapters distinguish unique index
      # constraint errors from other types of database errors by throwing an
219 220 221 222
      # ActiveRecord::RecordNotUnique exception. For other adapters you will
      # have to parse the (database-specific) exception message to detect such
      # a case.
      #
223
      # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception:
224
      #
225 226 227 228
      # * ActiveRecord::ConnectionAdapters::MysqlAdapter.
      # * ActiveRecord::ConnectionAdapters::Mysql2Adapter.
      # * ActiveRecord::ConnectionAdapters::SQLite3Adapter.
      # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.
229
      def validates_uniqueness_of(*attr_names)
230
        validates_with UniquenessValidator, _merge_attributes(attr_names)
231 232 233
      end
    end
  end
P
Pratik Naik 已提交
234
end