How to rescue model transaction and show the user an error?
def exchange_status_with(address)
ActiveRecord::Base.transaction do
self.save!
address.save!
end
rescue ActiveRecord::RecordInvalid => exception
# do something with exception here
end
FYI, an exception looks like:
#<ActiveRecord::RecordInvalid: Validation failed: Email can't be blank>
And:
exception.message
# => "Validation failed: Email can't be blank"
Side note, you can change self.save!
to save!
Alternate solution if you want to keep your active model errors:
class MyCustomErrorClass < StandardError; end
def exchange_status_with(address)
ActiveRecord::Base.transaction do
raise MyCustomErrorClass unless self.save
raise MyCustomErrorClass unless address.save
end
rescue MyCustomErrorClass
# here you have to check self.errors OR address.errors
end
Error Handling in ActiveRecord Transactions?
If you are using the save! method with a bang (exclamation point), the application will throw an exception when the save fails. You would then have to catch the exception to handle the failure.
begin
@ticket.transaction do
@ticket.save!
@user.save!
end
#handle success here
rescue ActiveRecord::RecordInvalid => invalid
#handle failure here
end
How to rollback all transactions in transaction block on error in Ruby on Rails
This is insanely overcomplicated and you have completely missunderstood how to use nested attributes:
class MissionsController
def create
@mission = Mission.new(mission_attributes)
if @mission.save
redirect_to @mission
else
render :new
end
end
...
private
def mission_params
params.require(:mission)
.permit(
:param_1, :param_2, :param3,
addresses_attributes: [:foo, :bar, :baz],
phones_attributes: [:foo, :bar, :baz],
camera_spec_attributes: [:foo, :bar, :baz],
)
end
end
All the work is actually done automatically by the setters declared by accepts_nested_attributes
. You just pass the hash or array of hashes of whitelisted parameters to it and let it do its thing.
You can prevent the parent object from being saved if the child object is invalid by using validates_associated
:
class Mission < ApplicationRecord
# ...
validates_associated :addresses
end
This just adds the error key “Phone is invalid” to the errors which isn't very user friendly. If you want to display the error messages per nested record you can get the object wrapped by the form builder when using fields_for
:
# app/shared/_errors.html.erb
<div id="error_explanation">
<h2><%= pluralize(object.errors.count, "error") %> prohibited this <%= object.model_name.singular %> from being saved:</h2>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
...
<%= f.fields_for :address_attributes do |address_fields| %>
<%= render('shared/errors', object: address_fields.object) if address_fields.object.errors.any? %>
<% end %>
self.errors[:base] inside an ActiveRecord transaction block
@TheJKFever, I am not sure that your code supposed to jump to any rescue block (well, and I actually confirmed by running it).
Your some_validation method returns false, and therefore "unless @user.some_validation" evaluates to true, and the render is executed with the following log output:
Completed 402 Payment Required in 128ms
{"error":{"base":["Some error message"]}}
You can refer to ActiveRecord::RecordInvalid API for details about RecordInvalid. Namely, "Raised by save! and create! when the record is invalid".
So, your "rescue ActiveRecord::RecordInvalid => exception" is supposed to handle exceptions in the "@some_instance.save!" statement and not in your custom validation.
In your validation you don't actually have the code that raises the ActiveRecord::RecordInvalid exception and probably fails with another error, which is easy to check by outputing it in details.
In order to use some_validation with "self.errors[:base] <<" properly first your need to add the following statement to your user model:
validate :some_validation
In this case, if you call "@user.save!" instead of "@some_instance.save!", you would fall into that "rescue ActiveRecord::RecordInvalid => exception" block.
PS: @TheJKFever, I saw one of your comments below and wanted to clarify something. A validation has a well defined purpose to validate a model before saving, and what you need then is not a model validation. What you actually need is a before_action on your controller that will check that your user is ready to be used in such and such action (consider it as controller validation). And yes, you probably will need some method on your user model to do that check.
Updated (after question update)
@TheJKFever, as I mentioned earlier, when I implemented your code I was able to execute "return render json: { error: @user.errors }...". So, if it fails for you, it must be due to some exception during meets_criteria call, but it is not RecordInvalid exception. Since you wrapped meets_criteria into transaction, it means that it probably does some database changes that you want to rollback if @some_instance.save! was unsuccessful. It would be a good idea to wrap your meets_criteria with the same rescue blocks too and find out. Do you create! or save! something in meets_criteria? If so, then it also can throw RecordInvalid exception. The problem is that in your code RecordInvalid can be thrown by meets_criteria and by @some_instance.save!, and there is no way to see in the code which one. In any case, if you wrap your meets_criteria with rescue, you will be able to send your render errors request from it. And if you decide to go with before_action filter then you will have to move whole your transaction into it (assuming that it requires the integrity of the data).
The point is that ActiveRecord::RecordInvalid exception will only be thrown in case of save! or create! failure due to invalid data. It might not be the case in your code, and some other exception is thrown, and you end up in "rescue => error" block.
Multi model saving, how to wrap in transaction and report errors
Let's start from the beginning.
We want our registration form object to have the same API as any other ActiveRecord model:
// view.html
<%= form_for(@book) do |f| %>
<% end %>
# controller.rb
def create
@book = Book.new(book_params)
if @book.save
redirect_to @book, notice: 'Book was successfully created.'
else
render :new
end
end
To do that, we create the following object:
class RegForm
include ActiveModel::Model
attr_accessor :company_name, :email, :password
def save
# Save Location and Account here
end
end
Now, by including ActiveModel::Model
, our RegForm
gains a ton of functionality, including showing errors and validating attributes (yes, it's unnecessary to include ActiveModel::Validations
). In this next step we add some validations:
validates :email, :password, presence: true
And we change save
so that it runs those validations:
def save
validate
# Save Location and Account here
end
validate
returns true
if all validations pass and false
otherwise.
validate
also adds errors to the @reg_form
(All ActiveRecord models have an errors
hash which is populated when a validation fails). This means that in the view we can do any of these:
@reg_form.errors.messages
#=> { email: ["can't be blank"], password: ["can't be blank"] }
@reg_form.errors.full_messages
#=> ["Email can't be blank", "Password can't be blank"]
@reg_form.errors[:email]
#=> ["can't be blank"]
@reg_form.errors.full_messages_for(:email)
#=> ["Email can't be blank"]
Meanwhile, our RegistrationsController
should look something like this:
def create
@reg_form = RegForm.new(reg_params)
if @reg_form.save
redirect_to @reg_form, notice: 'Registration was successful'
else
render :new
end
end
We can clearly see that when @reg_form.save
returns false
, the new
view is re-rendered.
Finally, we change save
so that our models save
calls are wrapped inside a transaction:
def save
if valid?
ActiveRecord::Base.transaction do
location = Location.create!(location_params)
account = location.create_account!(account_params)
end
true
end
rescue ActiveRecord::StatementInvalid => e
# Handle database exceptions not covered by validations.
# e.message and e.cause.message can help you figure out what happened
end
Points worthy of note:
create!
is used instead ofcreate
. The transaction is only rolled back if an exception is raised (which methods with a bang usually do).validate
is just an alias forvalid?
. To avoid all that indentation we could instead use a guard at the top of thesave
method:return if invalid?
We can turn a database exception (like an email uniqueness constraint) into an error by doing something like:
rescue ActiveRecord::RecordNotUnique
errors.add(:email, :taken)
endWe can add an error not directly associated with an attribute by using the symbol
:base
:errors.add(:base, 'Company and Email do not match')
Related Topics
Can't Use Compass After Installing It
What Is Double Method in Rspec For
Better Way to Turn a Ruby Class into a Module Than Using Refinements
How to Reflect in the Database a New Belongs_To and Has_Many Relationship in Ruby on Rails
Why Are Constants from Extended Module Not Available in Class Methods Declared with Self
How to Install Gems with Apt-Get on Ubuntu
How to Use an Actionview::Helper in a Ruby Script, Outside of Rails
Getting a Dns Txt Record in Ruby
Sorting a Two-Dimensional Array by Second Value
Can't Convert Fixnum to String During Rake Db:Create
No Implicit Conversion of String into Integer (Typeerror)
Creating an Md5 Hash of a Number, String, Array, or Hash in Ruby