ActiveRecord transactions not raising errors - ruby-on-rails

I've set up an ActiveRecord transaction, however when the second statement fails, it's not causing the transaction to fail. Here's my code:
Contact.transaction do
contact = Contact.create(params)
channel = ContactChannel.create(contact: contact, phone: contact.phone)
# ContactChannel query raises a validation error
# puts "ERRORS: #{channel.errors.messages}" outputs the following:
# {:channel_key=>["has already been taken"]}
contact # Still returns the contact that was created
end
Any idea why this doesn't fail despite the validation error?

create! instead of create should raise an exception, which should cause the transaction to be rolled back. It is basically a more strict version, and if no exception is raised, the transaction does not fail.
To get the reason for the transaction rollback, you can wrap your Transaction-statement in an begin (...) rescue - block and catch the ActiveRecord::Rollback- error and use its message to return the reason for the transaction failure.

Related

How to know why a transaction was rolled back?

I have an Account model. It has usual validations and after_save callbacks. There is a requirement, and account objects are to be created following some additional validation strategy. I have the following snippet, which works fine:
def special_account_creation
Account.transaction do
a = Account.create(params)
resp = update_third_party(a) # Throws exception if unable to update
validate_amount(a, resp) # Throws exception if `a` is invalid
# rescue Rollback ??? <--- How to do this
# msg = <transaction_error_details>
# Rails.logger.info("Special Account not created due to: #{msg}")
end
end
a account is destroyed if anything goes wrong while calling a third party API or during validation.
I want to know how I can log the error to know why the transaction was rolled back.
ActiveRecord transaction catches any exception being raised inside and rolls back the transaction. After that, if the exception is ActiveRecord::Rollback, it is supressed otherwise any other exception is raised above the call stack.
Ref: https://api.rubyonrails.org/classes/ActiveRecord/Rollback.html
So you could do something like this:
Account.transaction do
begin
a = Account.create(params)
resp = update_third_party(a) # Throws exception if unable to update
validate_amount(a, resp) # Throws exception if `a` is invalid
rescue StandardError => e # <- assuming all errors need to be logged
Rails.logger.info("Special Account not created due to: #{msg}")
raise e # <-- Don't forget to raise the error!
end
end
I suggest raise the error from the rescue block - this will rollback the transaction and if the error is something other than ActiveRecord::Rollback, it will be raised outside the transaction block. These errors you can handle in rescue_from block in base controller.
Also prefer catching StandardError and not Exception: https://robots.thoughtbot.com/rescue-standarderror-not-exception
I am assuming we want to catch all possible errors in that block, hence the use of StandardError, otherwise use any specific errors that need to be intercepted.
ActiveRecord does not raise Rollback on failed update and/or failed validation.
You should rescue what was raised and then there are two opportunities: either raise Rollback to just rollback a transaction and continue normal execution flow, or re-raise what was raised to indeed break the normal flow.
def special_account_creation
Account.transaction do
a = Account.create(params)
begin
resp = update_third_party(a) # Throws exception if unable to update
validate_amount(a, resp) # Throws exception if `a` is invalid
rescue => e # might be more specific
Rails.logger.info("Transaction failed with a message #{e.message}")
raise ActiveRecord::Rollback, e.message # just to rollback
# raise e # for re-raising the exception
end
end
end

why is a transaction rolled back if it exists in a rescue/raise clause in Rails

I have a simple exception handler as follows
begin
# code
rescue Exception
# Write to database
raise
end
The write to database is rolled back if raise is called. Is what I'm attempting to do possible?
Edit
Write to database does the following
Question.create(
notification_id: 1,
text: 'test'
)
Very simple.
You cannot rollback unless you use a transactions like follows
raise ActiveRecord::Rollback, "Call tech support!"
In your case, May be you have validated attributes in model (Question) and it get failed.
You can check errors like :
questions=Questions.new(...)
errors = questions.errors.full_messages if questions.invalid?

Correctly handling errors from a Database Transaction in your Rails Model

I have a method called invite! in my user model which will generate a unique token for a user and email them an invitation to join / sign up for my app.
The simplified implementation is -
class User < ActiveRecord::Base
# ...
# ...
def invite!
transaction do
generate_invitation_token
mark_as_invited
save!
end
WelcomeMailer.delay.send_invitation(id)
end
end
As you can see, it does some setup steps inside a transaction and then sends a WelcomeMailer invitation.
The key here is that I do NOT want an email delivered if there is any issue in updating the database during the transaction.
How do I best implement some error handling here?
The ruby-level method could throw an error. For example save! might error because of some model validation. In that case I can rescue it and return before it gets to the mailer.
What happens if there's some DB level transaction that errors out? Does it bubble up and throw an error in ruby? Will the same begin..rescue approach work there as well?
Thanks!
Two points about your case:
"RecordInvalid - raised by ActiveRecord::Base#save! and ActiveRecord::Base.create! when the record is invalid." (link)
"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." (link)
That means, what in a case of an error inside the transaction block in your current simplified code next things will happen:
the transaction will be rolled back
a mail will not be sent
the error will be throwed upper from invite!
If it's necessary to make some actions in a case of an error inside the transaction block, you may change the code like this:
def invite!
transaction do
generate_invitation_token
mark_as_invited
save!
end
rescue RecordInvalid => e
# some actions
raise e
else
# code that runs only if *no* exception was raised
WelcomeMailer.delay.send_invitation(id)
end

rails db transaction not rolling back if one of the db updates fails

I have the following method in my model:
class Task < ActiveRecord::Base
def update_completed_task(task_params, completed_material_params)
puts 'in update_completed_task method'
transaction do
begin
puts 'inside transaction'
self.task_finished
puts 'after task finished'
self.update_attributes!(task_params)
puts 'after update_attributes'
if completed_material_params
completed_material_params.each do |key, value|
#completed_material = CompletedMaterial.where("identity = ?", value).first
#completed_material.task = self
#completed_material.save
end
end
puts 'affter loop'
UserNotification.freelancer_has_submitted_documents(self.schedule.project, self)
puts 'after user notification change'
rescue
puts 'in rescue again yolo gandi rollback'
end
end
end
end
I am new to transactions in rails, but my understanding was that if one of the database interactions failed, the whole transaction would be rolled back. In the code above, the line:
self.update_attributes(task_params)
is failing, so the database update that occurs from self.task_finished on the line before should be rolled back. For some reason, it is not rolled back.
For background information, although i don't think it should make a difference, the "self.task_finished" line uses the state_machine gem to change the state of task. It should still rollback though. What is wrong with my transaction
You're misunderstanding what constitutes "failure"; record validations aren't "failure", they're a normal part of using your app, and they don't force any kind of database rollback.
You need to either explicitly cancel the transaction, or leave the transaction block via an exception, for the transaction to fail. Currently, you're successfully reaching the end of the transaction, so everything is happily committed to the database.
As suggested already, the best solution is update_attributes! which makes your validations into real failure by throwing an exception. As for your problem with update_attributes!...
tried that and it did do the rollback, but it also stops the program from running and the error messages don't display
That's the point. If your update_attributes! fails, there's no reason to proceed with the rest of your code, as all that does is create/update new records. The point of the transactions is to roll those changes back anyways, so the exception is doing its job perfectly by preventing that code from running.
If your validation errors aren't displaying, you should handle the exception and render normally to prevent Rails from rendering an error page when the exception leaves your method.

Ruby on Rails: how do I catch ActiveRecord::Rollback?

In my controller, I have code that looks like the following:
#mymodel.transaction do
for a in arr
#mymodel.some_method(a)
end
end
in #mymodel#some_method I could throw an ActiveRecord::Rollback exception which in the db does what it needs to do, however I then simply get an HTTP 500 and no way to catch the exception to let the user know in an elegant way what went wrong.
I've tried wrapping #mymodel.transaction do in a begin/rescue block, but that won't do it either. What's the best way to catch the exception so I can present the proper view to the user?
From the ActiveRecord::Base documentation:
Normally, raising an exception will cause the transaction method to rollback the database transaction and pass on the exception. But if you raise an ActiveRecord::Rollback exception, then the database transaction will be rolled back, without passing on the exception.
A small example:
class ThrowController < ApplicationController
def index
status = ActiveRecord::Base.connection.transaction do
raise ActiveRecord::Rollback.new
end
Rails.logger.info "followed transaction"
end
end
then:
>> c = ThrowController.new.index
=> "followed transaction \n"
As you can see, the ActiveRecord:::Rollback exception is swallowed by the transaction block.
It seems to me that something else is going on with your code that we're not aware of.

Resources