tallty_duck_record 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +41 -0
- data/README.md +82 -0
- data/Rakefile +28 -0
- data/lib/core_ext/array_without_blank.rb +46 -0
- data/lib/duck_record.rb +65 -0
- data/lib/duck_record/associations.rb +130 -0
- data/lib/duck_record/associations/association.rb +271 -0
- data/lib/duck_record/associations/belongs_to_association.rb +71 -0
- data/lib/duck_record/associations/builder/association.rb +127 -0
- data/lib/duck_record/associations/builder/belongs_to.rb +44 -0
- data/lib/duck_record/associations/builder/collection_association.rb +45 -0
- data/lib/duck_record/associations/builder/embeds_many.rb +9 -0
- data/lib/duck_record/associations/builder/embeds_one.rb +9 -0
- data/lib/duck_record/associations/builder/has_many.rb +11 -0
- data/lib/duck_record/associations/builder/has_one.rb +20 -0
- data/lib/duck_record/associations/builder/singular_association.rb +33 -0
- data/lib/duck_record/associations/collection_association.rb +476 -0
- data/lib/duck_record/associations/collection_proxy.rb +1160 -0
- data/lib/duck_record/associations/embeds_association.rb +92 -0
- data/lib/duck_record/associations/embeds_many_association.rb +203 -0
- data/lib/duck_record/associations/embeds_many_proxy.rb +892 -0
- data/lib/duck_record/associations/embeds_one_association.rb +48 -0
- data/lib/duck_record/associations/foreign_association.rb +11 -0
- data/lib/duck_record/associations/has_many_association.rb +17 -0
- data/lib/duck_record/associations/has_one_association.rb +39 -0
- data/lib/duck_record/associations/singular_association.rb +73 -0
- data/lib/duck_record/attribute.rb +213 -0
- data/lib/duck_record/attribute/user_provided_default.rb +30 -0
- data/lib/duck_record/attribute_assignment.rb +118 -0
- data/lib/duck_record/attribute_decorators.rb +89 -0
- data/lib/duck_record/attribute_methods.rb +325 -0
- data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
- data/lib/duck_record/attribute_methods/dirty.rb +107 -0
- data/lib/duck_record/attribute_methods/read.rb +78 -0
- data/lib/duck_record/attribute_methods/serialization.rb +66 -0
- data/lib/duck_record/attribute_methods/write.rb +70 -0
- data/lib/duck_record/attribute_mutation_tracker.rb +108 -0
- data/lib/duck_record/attribute_set.rb +98 -0
- data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
- data/lib/duck_record/attributes.rb +262 -0
- data/lib/duck_record/base.rb +300 -0
- data/lib/duck_record/callbacks.rb +324 -0
- data/lib/duck_record/coders/json.rb +13 -0
- data/lib/duck_record/coders/yaml_column.rb +48 -0
- data/lib/duck_record/core.rb +262 -0
- data/lib/duck_record/define_callbacks.rb +23 -0
- data/lib/duck_record/enum.rb +139 -0
- data/lib/duck_record/errors.rb +71 -0
- data/lib/duck_record/inheritance.rb +130 -0
- data/lib/duck_record/locale/en.yml +46 -0
- data/lib/duck_record/model_schema.rb +71 -0
- data/lib/duck_record/nested_attributes.rb +555 -0
- data/lib/duck_record/nested_validate_association.rb +262 -0
- data/lib/duck_record/persistence.rb +39 -0
- data/lib/duck_record/readonly_attributes.rb +36 -0
- data/lib/duck_record/reflection.rb +650 -0
- data/lib/duck_record/serialization.rb +26 -0
- data/lib/duck_record/translation.rb +22 -0
- data/lib/duck_record/type.rb +77 -0
- data/lib/duck_record/type/array.rb +36 -0
- data/lib/duck_record/type/array_without_blank.rb +36 -0
- data/lib/duck_record/type/date.rb +7 -0
- data/lib/duck_record/type/date_time.rb +7 -0
- data/lib/duck_record/type/decimal_without_scale.rb +13 -0
- data/lib/duck_record/type/internal/abstract_json.rb +33 -0
- data/lib/duck_record/type/internal/timezone.rb +15 -0
- data/lib/duck_record/type/json.rb +6 -0
- data/lib/duck_record/type/registry.rb +97 -0
- data/lib/duck_record/type/serialized.rb +63 -0
- data/lib/duck_record/type/text.rb +9 -0
- data/lib/duck_record/type/time.rb +19 -0
- data/lib/duck_record/type/unsigned_integer.rb +15 -0
- data/lib/duck_record/validations.rb +67 -0
- data/lib/duck_record/validations/subset.rb +74 -0
- data/lib/duck_record/validations/uniqueness_on_real_record.rb +248 -0
- data/lib/duck_record/version.rb +3 -0
- data/lib/tasks/acts_as_record_tasks.rake +4 -0
- metadata +181 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module DuckRecord
|
2
|
+
module Type
|
3
|
+
class Time < ActiveModel::Type::Time
|
4
|
+
include Internal::Timezone
|
5
|
+
|
6
|
+
class Value < DelegateClass(::Time) # :nodoc:
|
7
|
+
end
|
8
|
+
|
9
|
+
def serialize(value)
|
10
|
+
case value = super
|
11
|
+
when ::Time
|
12
|
+
Value.new(value)
|
13
|
+
else
|
14
|
+
value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "duck_record/validations/uniqueness_on_real_record"
|
2
|
+
require "duck_record/validations/subset"
|
3
|
+
|
4
|
+
module DuckRecord
|
5
|
+
class RecordInvalid < DuckRecordError
|
6
|
+
attr_reader :record
|
7
|
+
|
8
|
+
def initialize(record = nil)
|
9
|
+
if record
|
10
|
+
@record = record
|
11
|
+
errors = @record.errors.full_messages.join(", ")
|
12
|
+
message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"errors.messages.record_invalid")
|
13
|
+
else
|
14
|
+
message = "Record invalid"
|
15
|
+
end
|
16
|
+
|
17
|
+
super(message)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# = Active Record \Validations
|
22
|
+
#
|
23
|
+
# Active Record includes the majority of its validations from ActiveModel::Validations
|
24
|
+
# all of which accept the <tt>:on</tt> argument to define the context where the
|
25
|
+
# validations are active. Active Record will always supply either the context of
|
26
|
+
# <tt>:create</tt> or <tt>:update</tt> dependent on whether the model is a
|
27
|
+
# {new_record?}[rdoc-ref:Persistence#new_record?].
|
28
|
+
module Validations
|
29
|
+
extend ActiveSupport::Concern
|
30
|
+
include ActiveModel::Validations
|
31
|
+
|
32
|
+
# Runs all the validations within the specified context. Returns +true+ if
|
33
|
+
# no errors are found, +false+ otherwise.
|
34
|
+
#
|
35
|
+
# Aliased as #validate.
|
36
|
+
#
|
37
|
+
# If the argument is +false+ (default is +nil+), the context is set to <tt>:default</tt>.
|
38
|
+
#
|
39
|
+
# \Validations with no <tt>:on</tt> option will run no matter the context. \Validations with
|
40
|
+
# some <tt>:on</tt> option will only run in the specified context.
|
41
|
+
def valid?(context = nil)
|
42
|
+
context ||= default_validation_context
|
43
|
+
output = super(context)
|
44
|
+
errors.empty? && output
|
45
|
+
end
|
46
|
+
|
47
|
+
def valid!(context = nil)
|
48
|
+
if valid?(context)
|
49
|
+
true
|
50
|
+
else
|
51
|
+
raise RecordInvalid.new(self)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
alias_method :validate, :valid?
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def default_validation_context
|
60
|
+
:default
|
61
|
+
end
|
62
|
+
|
63
|
+
def perform_validations(options = {})
|
64
|
+
options[:validate] == false || valid?(options[:context])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuckRecord
|
4
|
+
module Validations
|
5
|
+
class SubsetValidator < ActiveModel::EachValidator # :nodoc:
|
6
|
+
ERROR_MESSAGE = "An object with the method #include? or a proc, lambda or symbol is required, " \
|
7
|
+
"and must be supplied as the :in (or :within) option of the configuration hash"
|
8
|
+
|
9
|
+
def check_validity!
|
10
|
+
unless delimiter.respond_to?(:include?) || delimiter.respond_to?(:call) || delimiter.respond_to?(:to_sym)
|
11
|
+
raise ArgumentError, ERROR_MESSAGE
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate_each(record, attribute, value)
|
16
|
+
unless subset?(record, value)
|
17
|
+
record.errors.add(attribute, :subset, options.except(:in, :within).merge!(value: value))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def delimiter
|
24
|
+
@delimiter ||= options[:in] || options[:within]
|
25
|
+
end
|
26
|
+
|
27
|
+
def subset?(record, value)
|
28
|
+
return false unless value.respond_to?(:to_a)
|
29
|
+
|
30
|
+
enumerable = value.to_a
|
31
|
+
members =
|
32
|
+
if delimiter.respond_to?(:call)
|
33
|
+
delimiter.call(record)
|
34
|
+
elsif delimiter.respond_to?(:to_sym)
|
35
|
+
record.send(delimiter)
|
36
|
+
else
|
37
|
+
delimiter
|
38
|
+
end
|
39
|
+
|
40
|
+
(members & enumerable).size == enumerable.size
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module HelperMethods
|
45
|
+
# Validates whether the value of the specified attribute is available in a
|
46
|
+
# particular enumerable object.
|
47
|
+
#
|
48
|
+
# class Person < ActiveRecord::Base
|
49
|
+
# validates_inclusion_of :gender, in: %w( m f )
|
50
|
+
# validates_inclusion_of :age, in: 0..99
|
51
|
+
# validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list"
|
52
|
+
# validates_inclusion_of :states, in: ->(person) { STATES[person.country] }
|
53
|
+
# validates_inclusion_of :karma, in: :available_karmas
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# Configuration options:
|
57
|
+
# * <tt>:in</tt> - An enumerable object of available items. This can be
|
58
|
+
# supplied as a proc, lambda or symbol which returns an enumerable. If the
|
59
|
+
# enumerable is a numerical, time or datetime range the test is performed
|
60
|
+
# with <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. When using
|
61
|
+
# a proc or lambda the instance under validation is passed as an argument.
|
62
|
+
# * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
|
63
|
+
# * <tt>:message</tt> - Specifies a custom error message (default is: "is
|
64
|
+
# not included in the list").
|
65
|
+
#
|
66
|
+
# There is also a list of default options supported by every validator:
|
67
|
+
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
|
68
|
+
# See <tt>ActiveModel::Validations#validates</tt> for more information
|
69
|
+
def validates_subset_of(*attr_names)
|
70
|
+
validates_with SubsetValidator, _merge_attributes(attr_names)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
module DuckRecord
|
2
|
+
module Validations
|
3
|
+
class UniquenessOnRealRecordValidator < ActiveModel::EachValidator # :nodoc:
|
4
|
+
def initialize(options)
|
5
|
+
unless defined?(ActiveRecord)
|
6
|
+
raise NameError, "Not found Active Record."
|
7
|
+
end
|
8
|
+
|
9
|
+
if options[:conditions] && !options[:conditions].respond_to?(:call)
|
10
|
+
raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \
|
11
|
+
"Pass a callable instead: `conditions: -> { where(approved: true) }`"
|
12
|
+
end
|
13
|
+
|
14
|
+
@finder_class = if options[:class_name].present?
|
15
|
+
options[:class_name].safe_constantize
|
16
|
+
elsif options[:class].present? && options[:class].is_a?(Class)
|
17
|
+
options[:class]
|
18
|
+
else
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
unless @finder_class
|
23
|
+
raise ArgumentError, "Must provide one of option :class_name or :class."
|
24
|
+
end
|
25
|
+
unless @finder_class < ActiveRecord::Base
|
26
|
+
raise ArgumentError, ":class must be an Active Record model, but got #{@finder_class}."
|
27
|
+
end
|
28
|
+
if @finder_class.abstract_class?
|
29
|
+
raise ArgumentError, ":class can't be an abstract class."
|
30
|
+
end
|
31
|
+
|
32
|
+
@primary_key = options[:primary_key]
|
33
|
+
|
34
|
+
super({ case_sensitive: true }.merge!(options))
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_each(record, attribute, value)
|
38
|
+
table = @finder_class.arel_table
|
39
|
+
value = map_enum_attribute(@finder_class, attribute, value)
|
40
|
+
|
41
|
+
relation = build_relation(@finder_class, table, attribute, value)
|
42
|
+
if @primary_key.present? && record.respond_to?(@primary_key)
|
43
|
+
if @finder_class.primary_key
|
44
|
+
relation = relation.where.not(@finder_class.primary_key => record.send(@primary_key))
|
45
|
+
else
|
46
|
+
raise ActiveRecord::UnknownPrimaryKey.new(@finder_class, "Can not validate uniqueness for persisted record without primary key.")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
relation = scope_relation(record, table, relation)
|
50
|
+
relation = relation.merge(options[:conditions]) if options[:conditions]
|
51
|
+
|
52
|
+
if relation.exists?
|
53
|
+
error_options = options.except(:case_sensitive, :scope, :conditions)
|
54
|
+
error_options[:value] = value
|
55
|
+
|
56
|
+
record.errors.add(attribute, :taken, error_options)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def build_relation(klass, table, attribute, value) #:nodoc:
|
63
|
+
if reflection = klass._reflect_on_association(attribute)
|
64
|
+
attribute = reflection.foreign_key
|
65
|
+
value = value.attributes[reflection.klass.primary_key] unless value.nil?
|
66
|
+
end
|
67
|
+
|
68
|
+
# the attribute may be an aliased attribute
|
69
|
+
if klass.attribute_alias?(attribute)
|
70
|
+
attribute = klass.attribute_alias(attribute)
|
71
|
+
end
|
72
|
+
|
73
|
+
attribute_name = attribute.to_s
|
74
|
+
|
75
|
+
column = klass.columns_hash[attribute_name]
|
76
|
+
cast_type = klass.type_for_attribute(attribute_name)
|
77
|
+
value = cast_type.serialize(value)
|
78
|
+
value = klass.connection.type_cast(value)
|
79
|
+
|
80
|
+
comparison = if !options[:case_sensitive] && !value.nil?
|
81
|
+
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
|
82
|
+
klass.connection.case_insensitive_comparison(table, attribute, column, value)
|
83
|
+
else
|
84
|
+
klass.connection.case_sensitive_comparison(table, attribute, column, value)
|
85
|
+
end
|
86
|
+
if value.nil?
|
87
|
+
klass.unscoped.where(comparison)
|
88
|
+
else
|
89
|
+
bind = ActiveRecord::Relation::QueryAttribute.new(attribute_name, value, ActiveRecord::Type::Value.new)
|
90
|
+
klass.unscoped.where(comparison, bind)
|
91
|
+
end
|
92
|
+
rescue RangeError
|
93
|
+
klass.none
|
94
|
+
end
|
95
|
+
|
96
|
+
def scope_relation(record, table, relation)
|
97
|
+
Array(options[:scope]).each do |scope_item|
|
98
|
+
scope_value = if record.class._reflect_on_association(scope_item)
|
99
|
+
record.association(scope_item).reader
|
100
|
+
else
|
101
|
+
record._read_attribute(scope_item)
|
102
|
+
end
|
103
|
+
relation = relation.where(scope_item => scope_value)
|
104
|
+
end
|
105
|
+
|
106
|
+
relation
|
107
|
+
end
|
108
|
+
|
109
|
+
def map_enum_attribute(klass, attribute, value)
|
110
|
+
mapping = klass.defined_enums[attribute.to_s]
|
111
|
+
value = mapping[value] if value && mapping
|
112
|
+
value
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
module ClassMethods
|
117
|
+
# Validates whether the value of the specified attributes are unique
|
118
|
+
# across the system. Useful for making sure that only one user
|
119
|
+
# can be named "davidhh".
|
120
|
+
#
|
121
|
+
# class Person < ActiveRecord::Base
|
122
|
+
# validates_uniqueness_of :user_name
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# It can also validate whether the value of the specified attributes are
|
126
|
+
# unique based on a <tt>:scope</tt> parameter:
|
127
|
+
#
|
128
|
+
# class Person < ActiveRecord::Base
|
129
|
+
# validates_uniqueness_of :user_name, scope: :account_id
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# Or even multiple scope parameters. For example, making sure that a
|
133
|
+
# teacher can only be on the schedule once per semester for a particular
|
134
|
+
# class.
|
135
|
+
#
|
136
|
+
# class TeacherSchedule < ActiveRecord::Base
|
137
|
+
# validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# It is also possible to limit the uniqueness constraint to a set of
|
141
|
+
# records matching certain conditions. In this example archived articles
|
142
|
+
# are not being taken into consideration when validating uniqueness
|
143
|
+
# of the title attribute:
|
144
|
+
#
|
145
|
+
# class Article < ActiveRecord::Base
|
146
|
+
# validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') }
|
147
|
+
# end
|
148
|
+
#
|
149
|
+
# When the record is created, a check is performed to make sure that no
|
150
|
+
# record exists in the database with the given value for the specified
|
151
|
+
# attribute (that maps to a column). When the record is updated,
|
152
|
+
# the same check is made but disregarding the record itself.
|
153
|
+
#
|
154
|
+
# Configuration options:
|
155
|
+
#
|
156
|
+
# * <tt>:message</tt> - Specifies a custom error message (default is:
|
157
|
+
# "has already been taken").
|
158
|
+
# * <tt>:scope</tt> - One or more columns by which to limit the scope of
|
159
|
+
# the uniqueness constraint.
|
160
|
+
# * <tt>:conditions</tt> - Specify the conditions to be included as a
|
161
|
+
# <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup
|
162
|
+
# (e.g. <tt>conditions: -> { where(status: 'active') }</tt>).
|
163
|
+
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
|
164
|
+
# non-text columns (+true+ by default).
|
165
|
+
# * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
|
166
|
+
# attribute is +nil+ (default is +false+).
|
167
|
+
# * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the
|
168
|
+
# attribute is blank (default is +false+).
|
169
|
+
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
|
170
|
+
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
|
171
|
+
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
|
172
|
+
# proc or string should return or evaluate to a +true+ or +false+ value.
|
173
|
+
# * <tt>:unless</tt> - Specifies a method, proc or string to call to
|
174
|
+
# determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
|
175
|
+
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
176
|
+
# method, proc or string should return or evaluate to a +true+ or +false+
|
177
|
+
# value.
|
178
|
+
#
|
179
|
+
# === Concurrency and integrity
|
180
|
+
#
|
181
|
+
# Using this validation method in conjunction with
|
182
|
+
# {ActiveRecord::Base#save}[rdoc-ref:Persistence#save]
|
183
|
+
# does not guarantee the absence of duplicate record insertions, because
|
184
|
+
# uniqueness checks on the application level are inherently prone to race
|
185
|
+
# conditions. For example, suppose that two users try to post a Comment at
|
186
|
+
# the same time, and a Comment's title must be unique. At the database-level,
|
187
|
+
# the actions performed by these users could be interleaved in the following manner:
|
188
|
+
#
|
189
|
+
# User 1 | User 2
|
190
|
+
# ------------------------------------+--------------------------------------
|
191
|
+
# # User 1 checks whether there's |
|
192
|
+
# # already a comment with the title |
|
193
|
+
# # 'My Post'. This is not the case. |
|
194
|
+
# SELECT * FROM comments |
|
195
|
+
# WHERE title = 'My Post' |
|
196
|
+
# |
|
197
|
+
# | # User 2 does the same thing and also
|
198
|
+
# | # infers that their title is unique.
|
199
|
+
# | SELECT * FROM comments
|
200
|
+
# | WHERE title = 'My Post'
|
201
|
+
# |
|
202
|
+
# # User 1 inserts their comment. |
|
203
|
+
# INSERT INTO comments |
|
204
|
+
# (title, content) VALUES |
|
205
|
+
# ('My Post', 'hi!') |
|
206
|
+
# |
|
207
|
+
# | # User 2 does the same thing.
|
208
|
+
# | INSERT INTO comments
|
209
|
+
# | (title, content) VALUES
|
210
|
+
# | ('My Post', 'hello!')
|
211
|
+
# |
|
212
|
+
# | # ^^^^^^
|
213
|
+
# | # Boom! We now have a duplicate
|
214
|
+
# | # title!
|
215
|
+
#
|
216
|
+
# This could even happen if you use transactions with the 'serializable'
|
217
|
+
# isolation level. The best way to work around this problem is to add a unique
|
218
|
+
# index to the database table using
|
219
|
+
# {connection.add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index].
|
220
|
+
# In the rare case that a race condition occurs, the database will guarantee
|
221
|
+
# the field's uniqueness.
|
222
|
+
#
|
223
|
+
# When the database catches such a duplicate insertion,
|
224
|
+
# {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will raise an ActiveRecord::StatementInvalid
|
225
|
+
# exception. You can either choose to let this error propagate (which
|
226
|
+
# will result in the default Rails exception page being shown), or you
|
227
|
+
# can catch it and restart the transaction (e.g. by telling the user
|
228
|
+
# that the title already exists, and asking them to re-enter the title).
|
229
|
+
# This technique is also known as
|
230
|
+
# {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control].
|
231
|
+
#
|
232
|
+
# The bundled ActiveRecord::ConnectionAdapters distinguish unique index
|
233
|
+
# constraint errors from other types of database errors by throwing an
|
234
|
+
# ActiveRecord::RecordNotUnique exception. For other adapters you will
|
235
|
+
# have to parse the (database-specific) exception message to detect such
|
236
|
+
# a case.
|
237
|
+
#
|
238
|
+
# The following bundled adapters throw the ActiveRecord::RecordNotUnique exception:
|
239
|
+
#
|
240
|
+
# * ActiveRecord::ConnectionAdapters::Mysql2Adapter.
|
241
|
+
# * ActiveRecord::ConnectionAdapters::SQLite3Adapter.
|
242
|
+
# * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.
|
243
|
+
def validates_uniqueness_on_real_record_of(*attr_names)
|
244
|
+
validates_with UniquenessOnRealRecordValidator, _merge_attributes(attr_names)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|