Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Allow any ActiveRecord uniqueness validation(s) to be deferred, and only check after a failed save due to uniqueness violation error from the database.
# ./config/initializers/deferred_uniqueness_validations.rb
#
# This initializer adds support for deferred processing of
# ActiveRecord uniqueness validations. When a uniqueness
# validation is deferred using the :deferred => true option, it is
# not checked before save. Instead, if there is a uniqueness
# constraint defined for the column in the database, and if the
# save operation raises an ActiveRecord::RecordNotUnique
# exception, then the validation is performed so that the precise
# cause can be determined and the appropriate error information
# will be added to the #errors collection.
module ActiveRecord
# Adds support to ActiveRecord for deferred processing of
# uniqueness validations.
module WithDeferredUniquenessValidations
def ar_deferred_validations_on?
@ar_deferred_validations_on ||= false
end
def add_ar_deferred_validation(&b)
@deferred_ar_validations << b
end
# No obvious clean & reliable way to avoid this duplication...
def save(options={})
return super unless have_ar_deferred_validations?
start_ar_deferred_validations!
begin
return transaction(:requires_new => true){ super }
rescue ActiveRecord::RecordNotUnique
apply_ar_deferred_validations!
raise if errors.empty?
return false
ensure
stop_ar_deferred_validations!
end
end
def save!(options={})
return super unless have_ar_deferred_validations?
start_ar_deferred_validations!
begin
transaction :requires_new => true do
super
end
rescue ActiveRecord::RecordNotUnique
apply_ar_deferred_validations!
raise if errors.empty?
raise RecordInvalid.new(self)
ensure
stop_ar_deferred_validations!
end
end
private
def have_ar_deferred_validations?
self.class.validators.detect{|v| v.options[:deferred]} && true || false
end
def start_ar_deferred_validations!
@ar_deferred_validations_on = true
@deferred_ar_validations = []
end
def apply_ar_deferred_validations!
@deferred_ar_validations.each do |validation|
validation.call
end
end
def stop_ar_deferred_validations!
@deferred_ar_validations = nil
@ar_deferred_validations_on = false
end
end
module Validations
# Patches the uniqueness validator to utilize the :deferred => true
# option. Use this option when the uniqueness constraint is also
# enforced at the database level, and the validation should be initially
# skipped, and then processed after rollback if the save fails with an
# ActiveRecord::RecordNotUnique exception.
UniquenessValidator.class_eval do
def validate_each_with_deferrable(record, *args)
if record.ar_deferred_validations_on? && options[:deferred]
record.add_ar_deferred_validation do
validate_each_without_deferrable(record, *args)
end
else
validate_each_without_deferrable(record, *args)
end
end
alias_method_chain :validate_each, :deferrable
end
end
Base.class_eval do
include WithDeferredUniquenessValidations
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.