Rollback not triggering in rails transaction when catching errors - ruby-on-rails

I have some rails code in a controller action that has a transaction with a custom error.
When a speisfic error in a transaction happens I want to the transaction to rollback and render some json code, however I cannot seem to make the transaction rollback (it always commits) and then render some json. Here's my following code:
def controller_action
error_message = nil
begin
ActiveRecord::Base.transaction do
code_that_moifys_db_to_rollback
code_that_causes_custom_error
some_more_code_i_dont_want_to_run
end
rescue SomeCustomError::Error
error_message = 'this is an error message'
raise ActiveRecord::Rollback
end
rescue ActiveRecord::Rollback
# no rollback is getting triggered and data is now corrupt if json rendered
render :json => {error: true, message: error_message}
# however doing "raise 'xxxxx'" will cause a DB rollback in the transaction
end
How do I both:
render json
and force the transaction to rollback
I think I may be misunderstanding how rollbacks are triggered, the strangest thing is if I raise in the second rescue it will trigger the transaction to rollback.

I know I am too late to answer this question but some people might still be looking for an approach to its solution. So let's start with the problem and then head toward the solution.
In case you are in a hurry you can directly jump to the solution section.
Goal:
To render error response(other than saving object) and rollback transaction.
Problem context
In rails, if you have saving object errors(object.errors.present?) in a transaction then it will enter the rescue block (and you can handle raised exception in the rescue block) and your code will look something like this.
def controller_action
ActiveRecord::Base.transaction do
if object.save!
render_success_response('success message here')
else
render_error_response(object.errors)
end
rescue => e
render_error_response(e)
raise ActiveRecord::Rollback
end
I will define two methods here to render error and success responses that I will be using.
def render_error_response(errors)
render :json => {errors: errors}
end
def render_success_response(success_message)
render :json => {success_message: success_message}
end
Now in the above code, you have to manually rollback the transaction but if you want rails to rollback transaction for you then you can write the above code like this
def controller_action
begin
ActiveRecord::Base.transaction do
if object.save!
render_success_response('success message here')
else
render_error_response(object.errors)
end
rescue => e
render_error_response(e)
end
end
In the above code, rails will automatically rollback the transaction for you and will enter in the rescue block with an exception error.
Problem
But what if you have an errors array that comes in response to(call) a service or something and if errors are present in that array you want to render those errors and rollback the transaction? (as you know rails is not going to rollaback transaction for you this time as these errors are not saving object errors)
solution for versions below Rails 7
def controller_action
begin
ActiveRecord::Base.transaction do
response = ResponseFromService
if response[:errors].blank?
render_success_response('success message here')
else
render_error_response(response[:errors])
raise ActiveRecord::Rollback
end
rescue => e
render_error_response(e)
end
end
Solution for Rails 7
In rails 7 we got a new feature that's exactly for this problem and that's using the return keyword
so in rails 7 code will look something like this
def controller_action
begin
ActiveRecord::Base.transaction do
response = ResponseFromService
if response[:errors].blank?
render_success_response('success message here')
else
return render_error_response(response[:errors])
end
rescue => e
render_error_response(e)
end
end
Warning:
Note that the return keyword will silently(without raising an exception ) rollbacks the transaction so be careful while using it.

Related

How to debug inside a code block without skipping it

I'm attempting to debug my code, in order to see if #new_participant is instantiated, but when I place binding.pry around it as displayed below, it hops over the block, and placing the debugger within the block obviously doesn't work either. How do I debug this?
def create_participant!(case_file)
binding.pry
params = participant_params(case_file.case_file.id)
ActiveRecord::Base.transaction do
#new_participant = participant_clazz.create!(params)
assign_private_infos!(participant_id: new_participant.id)
binding.pry
end
call_link_participant_job!
end
You're calling create! which will raise an ActiveRecord::RecordInvalid exception if the record is not valid. Exceptions immediately halt the script execution and Ruby goes up the call stack until a rescue is found (or not).
ActiveRecord::Transactions wraps the block in a rescue which triggers a rollback and then propagates the exception.
Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you should be ready to catch those in your application code.
If you want to run code before the rollback you need to rescue the exception inside the block:
def create_participant!(case_file)
binding.pry
params = participant_params(case_file.case_file.id)
ActiveRecord::Base.transaction do
begin
#new_participant = participant_clazz.create!(params)
assign_private_infos!(participant_id: new_participant.id)
rescue ActiveRecord::RecordInvalid => e
binding.pry
raise e # re-raise the exception to trigger a rollback
end
call_link_participant_job!
end
end
Or you can rescue the exception after the rollback:
def create_participant!(case_file)
binding.pry
params = participant_params(case_file.case_file.id)
begin
ActiveRecord::Base.transaction do
#new_participant = participant_clazz.create!(params)
assign_private_infos!(participant_id: new_participant.id)
call_link_participant_job!
end
rescue ActiveRecord::RecordInvalid => e
binding.pry
end
end

Handling exceptions in rails

I am bit unclear about exception handling while using Active record transactions in rails. I have seen many of them using,
Method: 1
def update
ActiveRecord::Base.transaction do
begin
# Some logic
rescue StandardError => e
raise ActiveRecord::Rollback
end
end
end
and have seen the below logics in many of the places.
Method:2
def update
ActiveRecord::Base.transaction do
if object.update(update_params)
# success
else
# error handling
end
end
rescue => e
# error handling
end
What I think is the second method itself is enough. I thought that, Transaction itself will rollback if anything unexpected happens or any logical error inside the transaction and we can catch them and do whatever we want. Is catching exception inside the transaction and raising Rollback manually needed anywhere?. What is the difference between both the methods and in which case?
You don't need to manually to rollback the transaction the code below should be good enough
def update
ActiveRecord::Base.transaction do
foo.update(foo_update_params)
end
rescue ActiveRecord::RecordInvalid
# Handle your exception here
end
Have a look here for a better explanation.

ActiveRecord rollback and return RecordInvalid message

I want to save multiple objects, and rollback all if any of them fail. But I also want to render the ActiveRecord::RecordInvalid message so the user knows why it didn't save. How do I do this?
def save_multiple_things_or_error
ActiveRecord::Base.transaction do
thing_one.save!
thing_two.save!
rescue ActiveRecord::RecordInvalid => exception
# exception.message is what I want to render
raise ActiveRecord::Rollback
end
end
This doesn't work for a few reasons. I believe the rescue should be in a begin end block, but then if I raise the rollback, I lose the RecordInvalid exception.
you could try this one:
begin
ActiveRecord::Base.transaction do
thing_one.save!
thing_two.save!
end
rescue => e
raise ActiveRecord::Rollback
end
this works fine for my situation

Better way to do a few save in one tranaction in rails 3.2

Here is our code to save quite a few objects all at once within one transaction. What the code does is to create a new checkout record (for warehouse) and update each item (there may be a few of them) in stock. Since all save has to be either all or none, we put all the save within Rails transaction:
#checkout = RequisitionCheckoutx::Checkout.new(params[:checkout])
#checkout.last_updated_by_id = session[:user_id]
#checkout.checkout_by_id = session[:user_id]
#checkout.transaction do
params['ids'].each do |id|
params['out_qtys'].each do |qty| #ids passed in as a string of 'id'
stock_item = RequisitionCheckoutx.warehouse_class.find_by_id(id.to_i)
qty = qty.to_i
if stock_item.stock_qty >= qty
stock_item.stock_qty = stock_item.stock_qty - qty
stock_item.last_updated_by_id = session[:user_id]
begin
stock_item.save
rescue => e
flash[:notice] = t('Stock Item#=') + id.to_s + ',' + e.message
end
end
end unless params['out_qtys'].blank?
end unless params['ids'].blank?
if #checkout.save
redirect_to URI.escape(SUBURI + "/authentify/view_handler?index=0&msg=Successfully Saved!")
else
flash[:notice] = t('Data Error. Not Saved!')
render 'new'
end
end
We haven't run the test yet and the code looks not pretty. Is there a better way to handle this kind of batch save? Also should the rescue loop be removed for transaction?
The transaction block should be performed first and then you should deal with action response. Besides that, catching exception here is pointless, cause using save returns simply true or false. Your transaction should look like:
RequisitionCheckoutx::Checkout.transaction do
begin
#...
#...
stock_item.save! # it will raise RecordInvalid or RecordNotSaved if something goes wrong
#...
#...
#checkout.save!
rescue Exception => e
raise ActiveRecord::Rollback # it calls Rollback to the database
end
end
Now, using ActiveModel::Dirty you need to check if #checkout has been saved:
if !#checkout.changed?
redirect_to "/something"
else
flash[:notice] = t('Data Error. Not Saved!')
render 'new'
end

Is it possible to re-raise an ActiveRecord::Rollback exception from inside a transaction block?

In my rails app, I have a transaction block in a controller's create action in which I attempt to create two records.
def create
ActiveRecord::Base.transaction do
#foo = Foo.new(params[:foo].except(:bar))
raise ActiveRecord::Rollback unless #foo.save
#bar = Bar.new(params[:foo][:bar])
raise ActiveRecord::Rollback unless #bar.save
end
end
I'd like to be able to rescue from the Rollback so I can return an error indicating which save failed.
rescue ActiveRecord::Rollback => e
#foo.errors = e if #foo.errors.blank?
ensure
respond_with #foo
end
However, I never get into the rescue block. I assume this is because, as the rails documentation states, the transaction block eats the Rollback exception and doesn't re-raise it. Is there a way to force this exception to be re-raised?

Resources