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.
Related
I have below code
ActiveRecord::Base.transaction do
begin
account.save
# outer statement
begin
user.save
# inner statement
rescue StandardError
raise ActiveRecord::Rollback
end
rescue StandardError
raise ActiveRecord::Rollback
end
end
If there is an exception in 'inner statement', only 'user' will be rollbacked, right? 'account' won't be rollbacked in that case, isn't it?
In your code there is a single database transaction, and it is all-or-nothing. Rolling back a transaction will roll back all the changes made in that transaction, regardless of where you issue the rollback.
You can nest transactions as well, but be wary that by default transactions are squished together, so even if you add a second transaction inside first transaction:
ActiveRecord::Base.transaction do
begin
account.save
# outer statement
ActiveRecord::Base.transaction do
begin
user.save
# inner statement
rescue StandardError
raise ActiveRecord::Rollback
end
end
rescue StandardError
raise ActiveRecord::Rollback
end
end
This will still result in the single transaction, and rollback will cancel all the changes.
To ask for a real subtransaction, you need to add request_new: true to the inner transaction:
ActiveRecord::Base.transaction do
begin
account.save
# outer statement
ActiveRecord::Base.transaction(require_new: true) do
begin
user.save
# inner statement
rescue StandardError
raise ActiveRecord::Rollback
end
end
rescue StandardError
raise ActiveRecord::Rollback
end
end
However, at the moment the only database supporting true nested transaction is MS-SQL. Rails at the moment handles this using save points - so don't be confused by the logs.
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
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.
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
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?