rails 3.1: how can app handle different 'reasons' for ActiveRecord::RecordInvalid (for example, duplicate vs validation error)
Asked Answered
F

2

7

In my app, I sometimes create a User on the fly, and a user's email must be a valid format, and be unique.

I would like to redirect to different places depending on WHICH validation caused the error to be raised: invalid format vs duplicate.

In my code I have

    begin
      user.save!
      flash[:notice] = "Created new user #{email} with password #{password}"

    rescue ActiveRecord::RecordInvalid => e
      flash[:alert] = "Failed to create account because #{e.message}"
      redirect_to SOMEPLACE
    end

If the email is invalid format (such as "user@example") e.message is "Validation failed: Email is invalid"

If the email already exists in the table, e.message is "Validation failed: Email has already been taken"

I hate the idea of parsing e.message text to determine the reason... is there a better way for a rescue handler to detect the underlying reason a ActiveRecord::RecordInvalid exception was thrown?

P.S. I know in THIS example I can simply check ahead for the email already existing before doing a save, but I'm trying to understand the general solution to detecting and acting on different validation failures throwing the same exception.

Fraser answered 6/3, 2012 at 21:31 Comment(1)
Actually can't simply check ahead for the email already existing, ActiveRecord's uniqueness validations are subject to race conditions and broken by design. You must have a uniqueness constraint inside your database and you must be prepared to deal with the exception that it will raise (either from .save or .save!).Argo
H
1

The standard Rails way to do this is not to use the bang operator, which raises an exception, but to use the standard save method and check whether it returned true or false:

if @user.save
  flash[:notice] = "User created."
  redirect_to :action => :index
else
  flash[:alert] = "User could not be created."
  render :action => :new
end

And in your user creation view:

<% if @user.errors.any? %>
  <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
  </ul>
<% end %>
Hephaestus answered 6/3, 2012 at 21:47 Comment(1)
But that doesn't tell you why the save failed. If you have a unique constraint inside your database (which you should as AR's uniqueness validation is subject to race conditions) then you can have x.valid? being true but both x.save and x.save! can raise an exception because the database's constraint was violated.Argo
H
0

If I'm understanding what you are trying to do correctly, one way to do this would be to just assert against the presence of an error on the field level. e.g.

if user.errors[:field_name].present?
  redirect_to path_for_field_name_error
end

Alternatively, you define some mapping of what fields redirect where as a constant (e.g. REDIRECT_PATHS in which case you end up with something like:

redirect_to REDIRECT_PATHS[field_name] if user.errors[:field_name].present?

where you can just loop over the field_names.

Hammy answered 17/3, 2014 at 21:52 Comment(2)
In my example, multiple errors might occur for the same field email address might be invalid, or already used, for example.Fraser
K. If you can somehow use the ActiveRecord built-ins to handle everything that's not this DB-driven uniqueness constraint (which @mu is too short correctly points out does not always work as expected), then you might be able to just check against .valid? and peel off the errors that way, excepting this uniqueness constraint. Of course, if you have multiple constraints on other fields, it's still an issue and falls back to string parsing. Good luck, hopefully that is helpful?Hammy

© 2022 - 2024 — McMap. All rights reserved.