Skip to content

Instantly share code, notes, and snippets.

@stevecj
Created March 7, 2013 21:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stevecj/5112001 to your computer and use it in GitHub Desktop.
Save stevecj/5112001 to your computer and use it in GitHub Desktop.
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