I am trying to execute two things inside a transaction and I'm not sure how I should test it in rspec. My code looks something like this:
Implementation:
def method
begin
ActiveRecord::Base.transaction do
...some implementation...
model1.save!
model2.save!
end
rescue => e
exception_info = {:class => e.class, :message => e.message, :backtrace => e.backtrace}
#logger.warn("Error. Rolling back.", :exception => exception_info)
end
end
Tests:
it "model1 object is not created if model2 fails to save" do
Model1.any_instance.should_receive(:save).and_raise("model1 save error!!")
method
Model2.all.should == []
end
it "" do
Model2.any_instance.should_receive(:save).and_raise("model2 save error!!")
method
Model1.all.should == []
end
I want both the models to be saved or none. My rspec tests check both the cases but I keep getting errors. If I add (:requires_new => true) to the transaction, it works. I thought it was meant for nested transactions, not something like this. Am I missing something?
ActiveRecord transactions only rollback if an exception is raised. Otherwise they persist whatever records were successfully created.
In your case, you want to use save! instead of save to interrupt the transaction. This will raise an ActiveRecord::RecordInvalid exception which you need to rescue and handle.
begin
ActiveRecord::Base.transaction do
...some implementation...
model1.save!
model2.save!
end
rescue ActiveRecord::RecordInvalid
end
Related
I have a class with following transaction:
# frozen_string_literal: true
class InactivateEmployee
include ServiceResult
def call(id)
begin
ActiveRecord::Base.transaction do
employee = Employee.find(id)
employee.update(is_active: false)
if employee.tasks.any?
employee.tasks.delete_all
end
response(code: 204, value: employee)
rescue ActiveRecord::ActiveRecordError
raise ActiveRecord::Rollback
end
rescue ActiveRecord::Rollback => e
response(code: 422, errors: e)
end
end
end
where ServiceResult is:
# frozen_string_literal: true
# ServiceResult should be included in each Service Class to have a unified returned object from each service
ServiceResultResponse = Struct.new(:success?, :response_code, :errors, :value, keyword_init: true)
module ServiceResult
def response(code:, errors: nil, value: nil )
ServiceResultResponse.new(
success?: code.to_s[0] == '2',
response_code: code,
errors: errors,
value: value
)
end
end
Question 1:
Is this code ok? what could be improved?
Question 2
How to test this transaction with use of Rspec? how to simulate in my test that destroy_all raise and error? i tried sth like that - but it does not work....
before do
allow(ActiveRecord::Associations::CollectionAssociation).to receive(:delete_all).and_return(ActiveRecord::ActiveRecordError.new)
end
Question 1: Is this code ok? what could be improved?
First and foremost, call should not be determining response codes. That wields together making the call with a specific context. That's someone else's responsibility. For example, 422 seems inappropriate, the only possible errors here are not finding the Employee (404) or an internal error (500). In general if you're rescuing ActiveRecordError you could probably be rescuing something more specific.
Does this need to be an entire service object? It's not using a service. It's only acting on Employee. If it's a method of Employee it can be used on any existing Employee object.
class Employee
def deactivate!
# There's no need for the find to be inside the transaction.
transaction do
# Use update! so it will throw an exception if it fails.
update!(is_active: false)
# Don't check first, it's an extra query and a race condition.
tasks.delete_all
end
end
end
Something else is responsible for catching errors and determining response codes. Probably the controller. Generic errors like a database failure should be handled higher up, probably by a default template.
begin
employee = Employee.find(id)
employee.deactivate!
rescue ActiveRecord::RecordNotFound
render status: :not_found
end
render status: :no_content
In ServiceResult you're checking success with code.to_s[0] == '2', use math or a Range instead. The caller should not be doing that at all, but it does because you have a module returning a Struct which can't do anything for itself.
ServiceResult should a class with a success? method. It's more flexible, more obvious what's happening, and doesn't pollute the caller's namespace.
class ServiceResult
# This makes it act like a Model.
include ActiveModel::Model
# These will be accepted by `new`
# You had "errors" but it takes a single error.
attr_accessor :code, :error, :value
def success?
(200...300).include?(code)
end
end
result = ServiceResult.new(code: 204, error: e)
puts "Huzzah!" if result.success?
I question if it's needed at all. It seems to be usurping the functionality of render. Is it an artifact of InactivateEmployee trying to do too much and having to pass its interpretation of what happened around?
Question 2 How to test this transaction with use of Rspec? how to simulate in my test that destroy_all raise and error?
Now that you're not doing too much in a single method, it's much simpler.
describe '#deactivate!' do
context 'with an active employee' do
# I'm assuming you're using FactoryBot.
let(:employee) { create(:employee, is_active: true) }
context 'when there is an error deleting tasks' do
before do
allow(employee.tasks).to receive(:delete_all)
# Exceptions are raised, not returned.
.and_raise(ActiveRecord::ActiveRecordError)
end
# I'm assuming there's an Employee#active?
it 'remains active' do
# same as `expect(employee.active?).to be true` with better diagnostics.
expect(employee).to be_active
end
end
end
end
Upon finding a failed validation, invalid? will return false and exit.
If all validations pass, invalid? will return true and the code will continue.
Does the rescue code only run if all validations pass?
If so, what raised errors will it catch?
Lastly why is there no Begin?
def save
return false if invalid? # invalid? triggers validations
true
rescue ActiveRecord::StatementInvalid => e
# Handle exception that caused the transaction to fail
# e.message and e.cause.message can be helpful
errors.add(:base, e.message)
false
end
Does the rescue code only run if all validations pass?
Blockquote
No, it will run if the call of invalid? throws an exception of type StatementInvalid
what raised errors will it catch?
Blockquote
the call of invalid? here is what raises the error
why is there no Begin?
in ruby, you can remove begin if you rescue from any exception that is raised from methods body so
def method
begin
#some code
rescue
#handle
end
end
equal to
def method
some code
rescue
# handle
end
but the second syntax shorter and cleaner
Note: it doesn't same right to me to rescue from ActiveRecord::StatementInvalid
inside an override to save
I have an ActiveRecord model Account :
class Account < ActiveRecord::Base
attr_accessible :msisdn
validates_uniqueness_of :msisdn, :on => :create,
:message => "User Already Registered ."
end
And I have a controller which try to create an account :
begin
account = Account.create!(:msisdn => user)
rescue Exception => e
$LOG.error "Account #{user} : --> #{e.message}"
end
Now the e.message always return : Validation failed: Msisdn User Already Registered, how am I supposed just to get just the message alone like User Already Registered. please note that I'm not using views at all, I want to use it from controller, and I'm using Rails 3.
Thanks in advance
Add two things to "config/locale/en.yml":
en:
activerecord:
errors:
messages:
record_invalid: "%{errors}"
errors:
format: "%{message}"
(or corresponding translation files for whichever languages you happen to support).
Note: this was tested in rails 5, but a quick scan of rails 3 docs makes me think it'll work there too.
When valid? is called on any model (which happens from create/save/update_attributes) it populates an errors object on the model. Of course if you use a bang method (create!) then the assignment will never happen, so use a non bang method instead. See 3rd code snippet.
account = Account.new(:msisdn => user)
unless account.save #
# account.errors will be populated with errors
puts account.errors[:msisdn] # => ['User Already Registered']
end
Alternative using a bang method
account = Account.new(:msisdn => user)
begin
account.save!
rescue Exception
puts account.errors[:msisdn]
end
Edit:
Another alternative after looking at the rails api docs is to get the record from the exception as it stores a copy. This makes my original statement false.
ActiveRecord::RecordInvalid
(github)
begin
account = Account.create!(:msisdn => user)
rescue ActiveRecord::RecordInvalid => e
puts e.record.errors[:msisdn] # => ['User Already Registered']
end
I have a model that, when a record is inserted, needs to call a webservice.
If the webservice fails ( timeout or other failures ), them the save in database should also be reverted.
I used the after_save callback and tried to raise an ActiveRecord::Rollback when this kind of error happens.
Although it returns false on object.save, it doesn't rollback the transaction. What is the proper way of doing this?
How can i also make sure that the record won't be created?
Try to use before_save and return false from it.
Are you wrapping this in an Active Record Transaction block?
User.transaction do
User.create(:username => 'Kotori')
User.transaction(:requires_new => true) do
User.create(:username => 'Nemu')
raise ActiveRecord::Rollback
end
end
Also See:
http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
While creating a new object i am getting ActiveRecord::RecordNotSaved error on before_save.
But i want to fetch the proper message other than ActiveRecord::RecordNotSaved error message.
How may i fetch the proper error message and pass it to the rescue?
begin
#some logic
raise unless object.save!
rescue ActiveRecord::RecordNotSaved => e
# How may fetch proper message where my object is failing here ..
# like object.errors.message or something like that.
end
begin
#some logic
#object.save!
rescue ActiveRecord::RecordNotSaved => e
#object.errors.full_messages
end
Why raise the exception and not just check if save or not ?
unless object.save
object.errors
end