开发者

Handling Unique Record Exceptions in a Controller

开发者 https://www.devze.com 2023-02-10 08:27 出处:网络
I have a model called Subscription th开发者_高级运维at has a unique index on the fields [:email, :location]. This means one email address can subscribe per location.

I have a model called Subscription th开发者_高级运维at has a unique index on the fields [:email, :location]. This means one email address can subscribe per location.

In my model:

class Subscription < ActiveRecord::Base
  validates :email, :presence => true, :uniqueness => true, :email_format => true, :uniqueness => {:scope => :location}
end

In my create method. I want to handle the the exception ActiveRecord::RecordNotUnique differently than a regular error. How would I add that in to this generic create method?

  def create
    @subscription = Subscription.new(params[:subscription])
    respond_to do |format|
      if @subscription.save
        format.html { redirect_to(root_url, :notice => 'Subscription was successfully created.') }
      else
        format.html { render :action => 'new' }
      end
    end
  end


I don't think there is a way to have an exception thrown just for a single type of validation failure. Either you can do a save! which would raise exceptions for all save errors (including all validation errors) and have them handled separately.

What you can do is handle the exception ActiveRecord::RecordInvalid and match the exception message with Validation failed: Email has already been taken and then handle it separately. But this also means that you would have to handle other errors too.

Something like,

begin
  @subscription.save!
rescue ActiveRecord::RecordInvalid => e
  if e.message == 'Validation failed: Email has already been taken'
    # Do your thing....
  else
    format.html { render :action => 'new' }
  end
end
format.html { redirect_to(root_url, :notice => 'Subscription was successfully created.') }

I'm not sure if this is the only solution to this though.


You will want to use rescue_from

In your controller

 rescue_from ActiveRecord::RecordNotUnique, :with => :my_rescue_method

 ....

 protected

 def my_rescue_method
   ...
 end

However, wouldn't you want to invalidate your record rather than throwing an exception?


A couple things I would change about the validation:

  1. Do the presence, uniqueness, and format validations in separate validations. (Your uniqueness key in the attributes hash you are passing to "validates" is being overwritten in your validation). I would make it look more like:

    validates_uniqueness_of :email, :scope => :location

    validates_presence_of :email

    validates_format_of :email, :with => RFC_822 # We use global validation regexes

  2. Validations are Application level, one of the reasons you should separate them is because the presence and format validations can be done without touching the database. The uniqueness validation will touch the database, but won't use the unique index that you setup. Application level validations don't interact with the database internals they generate SQL and based on the query results make a determination of validity. You can leave the validates_uniqueness_of but be prepared for race conditions in your application.

Since the validation is application level it will request the row (something like "SELECT * FROM subscriptions WHERE email = 'email_address' LIMIT 1"), if a row is returned then the validation fails. If a row is not returned then it is considered valid.

However, if at the same time someone else signs up with the same email address and they both do not return a row before creating a new one then the 2nd "save" commit will trigger the uniqueness Database index constraint without triggering the validation in the application. (Since most likely they are running on different application servers or at least different VM's or processes).

ActiveRecord::RecordInvalid is raised when the validation fails, not when the unique index constraint on the database is violated. (There are multiple levels of ActiveRecord Exceptions that can be triggered at different points in the request/response lifecycle)

RecordInvalid is raised at the first level (Application level) whereas RecordNotUnique can be raised after the submission is attempted and the database server determines the transaction does not meet the index constraint. (ActiveRecord::StatementInvalid is the parent of the post fetch Exception that will be raised in this instance and you should rescue it if you are actually trying to get the database feedback and not the Application level validation)

If you are in your controller "rescue_from" (as outlined by The Who) should work just fine to recover from these different types of errors and it looks like the initial intent was to handle them differently so you can do so with multiple "rescue_from" calls.


Adding to Chirantans answer, with Rails 5 (or 3/4, with this Backport) you can also use the new errors.details:

begin
  @subscription.save!
rescue ActiveRecord::RecordInvalid => e
  e.record.errors.details
  # => {"email":[{"error":"taken","value":"user@example.org"}]}
end

Which is very handy for differentiating between the different RecordInvalid types and does not require relying on the exceptions error-message.

Note that it includes all errors reported by the validation-process, which makes handling multiple uniqueness-validation-errors much easier.

For example, you can check if all validation-errors for a model-attribute are just uniqueness-errors:

exception.record.errors.details.all? do |hash_element|
  error_details = hash_element[1]  
  error_details.all? { |detail| detail[:error] == :taken }
end


This gem rescues the constraint failure at the model level and adds a model error (model.errors) so that it behaves like other validation failures. Enjoy! https://github.com/reverbdotcom/rescue-unique-constraint

0

精彩评论

暂无评论...
验证码 换一张
取 消