mirror of https://github.com/rails/rails
Avoid validating a unique field if it has not changed and is backed by a unique index
This commit is contained in:
parent
3404606378
commit
c2bdc6b0c2
|
@ -1,3 +1,12 @@
|
|||
* Avoid validating a unique field if it has not changed and is backed by a unique index.
|
||||
|
||||
Previously, when saving a record, ActiveRecord will perform an extra query to check for the uniqueness of
|
||||
each attribute having a `uniqueness` validation, even if that attribute hasn't changed.
|
||||
If the database has the corresponding unique index, then this validation can never fail for persisted records,
|
||||
and we could safely skip it.
|
||||
|
||||
*fatkodima*
|
||||
|
||||
* Stop setting `sql_auto_is_null`
|
||||
|
||||
Since version 5.5 the default has been off, we no longer have to manually turn it off.
|
||||
|
|
|
@ -20,6 +20,8 @@ module ActiveRecord
|
|||
finder_class = find_finder_class_for(record)
|
||||
value = map_enum_attribute(finder_class, attribute, value)
|
||||
|
||||
return if record.persisted? && !validation_needed?(finder_class, record, attribute)
|
||||
|
||||
relation = build_relation(finder_class, attribute, value)
|
||||
if record.persisted?
|
||||
if finder_class.primary_key
|
||||
|
@ -64,6 +66,48 @@ module ActiveRecord
|
|||
class_hierarchy.detect { |klass| !klass.abstract_class? }
|
||||
end
|
||||
|
||||
def validation_needed?(klass, record, attribute)
|
||||
return true if options[:conditions] || options.key?(:case_sensitive)
|
||||
|
||||
scope = Array(options[:scope])
|
||||
attributes = scope + [attribute]
|
||||
attributes = resolve_attributes(record, attributes)
|
||||
|
||||
return true if attributes.any? { |attr| record.attribute_changed?(attr) ||
|
||||
record.read_attribute(attr).nil? }
|
||||
|
||||
!covered_by_unique_index?(klass, record, attribute, scope)
|
||||
end
|
||||
|
||||
def covered_by_unique_index?(klass, record, attribute, scope)
|
||||
@covered ||= self.attributes.map(&:to_s).select do |attr|
|
||||
attributes = scope + [attr]
|
||||
attributes = resolve_attributes(record, attributes)
|
||||
|
||||
klass.connection.schema_cache.indexes(klass.table_name).any? do |index|
|
||||
index.unique &&
|
||||
index.where.nil? &&
|
||||
(index.columns - attributes).empty?
|
||||
end
|
||||
end
|
||||
|
||||
@covered.include?(attribute.to_s)
|
||||
end
|
||||
|
||||
def resolve_attributes(record, attributes)
|
||||
attributes.flat_map do |attribute|
|
||||
reflection = record.class._reflect_on_association(attribute)
|
||||
|
||||
if reflection.nil?
|
||||
attribute.to_s
|
||||
elsif reflection.polymorphic?
|
||||
[reflection.foreign_key, reflection.foreign_type]
|
||||
else
|
||||
reflection.foreign_key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_relation(klass, attribute, value)
|
||||
relation = klass.unscoped
|
||||
comparison = relation.bind_attribute(attribute, value) do |attr, bind|
|
||||
|
|
|
@ -36,6 +36,10 @@ class ReplyWithTitleObject < Reply
|
|||
def title; ReplyTitle.new; end
|
||||
end
|
||||
|
||||
class TopicWithEvent < Topic
|
||||
belongs_to :event, foreign_key: :parent_id
|
||||
end
|
||||
|
||||
class TopicWithUniqEvent < Topic
|
||||
belongs_to :event, foreign_key: :parent_id
|
||||
validates :event, uniqueness: true
|
||||
|
@ -618,3 +622,173 @@ class UniquenessValidationTest < ActiveRecord::TestCase
|
|||
assert_equal(["has already been taken"], item2.errors[:id])
|
||||
end
|
||||
end
|
||||
|
||||
class UniquenessValidationWithIndexTest < ActiveRecord::TestCase
|
||||
self.use_transactional_tests = false
|
||||
|
||||
def setup
|
||||
@connection = Topic.connection
|
||||
@connection.schema_cache.clear!
|
||||
Topic.delete_all
|
||||
Event.delete_all
|
||||
end
|
||||
|
||||
def teardown
|
||||
Topic.clear_validators!
|
||||
@connection.remove_index(:topics, name: :topics_index, if_exists: true)
|
||||
end
|
||||
|
||||
def test_new_record
|
||||
Topic.validates_uniqueness_of(:title)
|
||||
@connection.add_index(:topics, :title, unique: true, name: :topics_index)
|
||||
|
||||
t = Topic.new(title: "abc")
|
||||
assert_queries(1) do
|
||||
t.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_changing_non_unique_attribute
|
||||
Topic.validates_uniqueness_of(:title)
|
||||
@connection.add_index(:topics, :title, unique: true, name: :topics_index)
|
||||
|
||||
t = Topic.create!(title: "abc")
|
||||
t.author_name = "John"
|
||||
assert_no_queries(ignore_none: false) do
|
||||
t.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_changing_unique_attribute
|
||||
Topic.validates_uniqueness_of(:title)
|
||||
@connection.add_index(:topics, :title, unique: true, name: :topics_index)
|
||||
|
||||
t = Topic.create!(title: "abc")
|
||||
t.title = "abc v2"
|
||||
assert_queries(1) do
|
||||
t.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_changing_non_unique_attribute_and_unique_attribute_is_nil
|
||||
Topic.validates_uniqueness_of(:title)
|
||||
@connection.add_index(:topics, :title, unique: true, name: :topics_index)
|
||||
|
||||
t = Topic.create!
|
||||
assert_nil t.title
|
||||
t.author_name = "John"
|
||||
assert_queries(1) do
|
||||
t.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_conditions
|
||||
Topic.validates_uniqueness_of(:title, conditions: -> { where.not(author_name: nil) })
|
||||
@connection.add_index(:topics, :title, unique: true, name: :topics_index)
|
||||
|
||||
t = Topic.create!(title: "abc")
|
||||
t.title = "abc v2"
|
||||
assert_queries(1) do
|
||||
t.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_case_sensitive
|
||||
Topic.validates_uniqueness_of(:title, case_sensitive: true)
|
||||
@connection.add_index(:topics, :title, unique: true, name: :topics_index)
|
||||
|
||||
t = Topic.create!(title: "abc")
|
||||
t.title = "abc v2"
|
||||
assert_queries(1) do
|
||||
t.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_partial_index
|
||||
skip unless @connection.supports_partial_index?
|
||||
|
||||
Topic.validates_uniqueness_of(:title)
|
||||
@connection.add_index(:topics, :title, unique: true, where: "approved", name: :topics_index)
|
||||
|
||||
t = Topic.create!(title: "abc")
|
||||
t.author_name = "John"
|
||||
assert_queries(1) do
|
||||
t.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_non_unique_index
|
||||
Topic.validates_uniqueness_of(:title)
|
||||
@connection.add_index(:topics, :title, name: :topics_index)
|
||||
|
||||
t = Topic.create!(title: "abc")
|
||||
t.author_name = "John"
|
||||
assert_queries(1) do
|
||||
t.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_scope
|
||||
Topic.validates_uniqueness_of(:title, scope: :author_name)
|
||||
@connection.add_index(:topics, [:author_name, :title], unique: true, name: :topics_index)
|
||||
|
||||
t = Topic.create!(title: "abc", author_name: "John")
|
||||
t.content = "hello world"
|
||||
assert_no_queries(ignore_none: false) do
|
||||
t.valid?
|
||||
end
|
||||
|
||||
t.author_name = "Amy"
|
||||
assert_queries(1) do
|
||||
t.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_uniqueness_on_relation
|
||||
TopicWithEvent.validates_uniqueness_of(:event)
|
||||
@connection.add_index(:topics, :parent_id, unique: true, name: :topics_index)
|
||||
|
||||
e1 = Event.create!(title: "abc")
|
||||
e2 = Event.create!(title: "cde")
|
||||
t = TopicWithEvent.create!(event: e1)
|
||||
|
||||
t.content = "hello world"
|
||||
assert_no_queries(ignore_none: false) do
|
||||
t.valid?
|
||||
end
|
||||
|
||||
t.event = e2
|
||||
assert_queries(1) do
|
||||
t.valid?
|
||||
end
|
||||
ensure
|
||||
TopicWithEvent.clear_validators!
|
||||
end
|
||||
|
||||
def test_index_of_sublist_of_columns
|
||||
Topic.validates_uniqueness_of(:title, scope: :author_name)
|
||||
@connection.add_index(:topics, :author_name, unique: true, name: :topics_index)
|
||||
|
||||
t = Topic.create!(title: "abc", author_name: "John")
|
||||
t.content = "hello world"
|
||||
assert_no_queries(ignore_none: false) do
|
||||
t.valid?
|
||||
end
|
||||
|
||||
t.author_name = "Amy"
|
||||
assert_queries(1, ignore_none: false) do
|
||||
t.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_index_of_columns_list_and_extra_columns
|
||||
Topic.validates_uniqueness_of(:title)
|
||||
@connection.add_index(:topics, [:title, :author_name], unique: true, name: :topics_index)
|
||||
|
||||
t = Topic.create!(title: "abc", author_name: "John")
|
||||
t.content = "hello world"
|
||||
assert_queries(1) do
|
||||
t.valid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue