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
Related
It is my understanding that wrapping .save! in ActiveRecord::Base.transaction will make sure all the models (User, Profile, and Setting) to save together or none at all.
However, I was also told that including .save! with all the models.save! methods will also do that. So essentially, both version 1 and 2 are the same. I have a feeling I am wrong, so what is the difference?
Thank you
Version 1
def save
if valid?
ActiveRecord::Base.transaction do
User.save!
Profile.save!
Setting.save!
end
else
false
end
end
Version 2
def save
if valid?
User.save!
Profile.save!
Setting.save!
else
false
end
end
Reference: https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
In the first case if any of save! statement fails then all the previous saved models will be rolled back. For ex:
If setting.save! fails then setting.save!, user.save! and profile.save! will be rolled back.
But in second case if any save! statement fails then it will only rollback that statement and it will also raise an exception. For ex:
If setting.save! fails then only setting.save! will be rolled back.
Both statements will work same only in 1 case when the first statement fails 'user.save!' as exception will be raised and in second case subsequent statement will not be executed
The difference between save and save! is that the latter will raise an exception but both will not save the value of object to table if validations fail.
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
Example:
BillingProfile.transaction do
if #billing_profile.save
unless SomeService.do_something # returns false and rollback occurs
raise ActiveRecord::Rollback
end
end
end
#billing_profile.persisted? # Still return true, despite rollback
#billing_profile.id # Is set, despite rollback
Why wouldn't the state of #billing_profile reflect that the record was rolled back?
This is a problem since the record cannot be created after it has been rolled back.
Turns out this was a bug in ActiveRecord (Rails 4): https://github.com/rails/rails/issues/13744
It has now been fixed.
I got interested in how transactions work. Your specific scenario is explained in the docs. Quoting the docs
transaction calls can be nested. By default, this makes all database
statements in the nested transaction block become part of the parent
transaction. For example, the following behavior may be surprising:
User.transaction do
User.create(username: 'Kotori')
User.transaction do
User.create(username: 'Nemu')
raise ActiveRecord::Rollback
end
end
creates both “Kotori” and “Nemu”. Reason is the ActiveRecord::Rollback
exception in the nested block does not issue a ROLLBACK. Since these
exceptions are captured in transaction blocks, the parent block does
not see it and the real transaction is committed.
In order to get a ROLLBACK for the nested transaction you may ask for
a real sub-transaction by passing requires_new: true. If anything goes
wrong, the database rolls back to the beginning of the sub-transaction
without rolling back the parent transaction. If we add it to the
previous example:
User.transaction do
User.create(username: 'Kotori')
User.transaction(requires_new: true) do
User.create(username: 'Nemu')
raise ActiveRecord::Rollback
end
end
only “Kotori” is created. This works on MySQL and PostgreSQL. SQLite3
version >= '3.6.8' also supports it.
Most databases don't support true nested transactions. At the time of
writing, the only database that we're aware of that supports true
nested transactions, is MS-SQL. Because of this, Active Record
emulates nested transactions by using savepoints on MySQL and
PostgreSQL. See dev.mysql.com/doc/refman/5.6/en/savepoint.html for
more information about savepoints.
In ActiveRecord model after_save callback I need to ROLLBACK transaction and return false.
def after_save_callback
if mycondition?
raise ActiveRecord::Rollback
end
end
This callback rolls-back transaction but mymodel.save! returns true. How to make it return false and rollback?
If you want to abort a save in an after_save callback, you should
raise ActiveRecord::RecordInvalid.new(self)
rather than
raise ActiveRecord::Rollback
This will not only roll back the transaction (callbacks always happen inside a possibly-implicit transaction as part of the save or create) but also cause save to return false.
Here is an article with more details: http://tech.taskrabbit.com/blog/2013/05/23/rollback-after-save/
def around_save
ActiveRecord::Base.transaction do
raise ActiveRecord::Rollback # this will actually ROLLBACK
yield # calls the actual save method
raise ActiveRecord::Rollback # this will cause a COMMIT!!! because it affect only this internal transaction.
# OTHER ACTIONS NOT EXECUTED BUT BEING A INTERNAL TRANSACTION, THE PARENT WILL COMMIT, because parent hasn't failed.
end
end
So... I think around_save come already on a transaction block, so you don't need to add that extra ActiveRecord::Base.transaction do block because rollbacks doesnt propagate up
So if you want to rollback before or after yield, you need to remove that internal transaction.
def around_save
#ActiveRecord::Base.transaction do
raise ActiveRecord::Rollback # this will actually ROLLBACK
yield # calls the actual save method
raise ActiveRecord::Rollback # this will actually ROLLBACK
# end
end
EDIT: Reading what I wrote... now seem difficult to understand. The point is: if you are gonna use aroud_save don't wrapp again with ActiveRecord::Base.transaction (do like in the last example) because rails will wrap the call to around_save with is own ActiveRecord::Base.transaction so when you raise ActiveRecord::Rollback you are only rolling back the most internal transaction, so you can end with extrange results and partial saves (like in the first example whic is FAIL).
I don't think you can do this with after_save You should be looking at around_save instead:
def around_save
ActiveRecord::Base.transaction do
yield # calls the actual save method
raise ActiveRecord::Rollback if my_condition?
end
end
I'd like to add a begin...rescue block to one of my controllers create method, in order to log better info and construct the correct error message to return to the client. Does the rescue in any way 'interrupt' the rollback process?
I'm assuming rails automatically does a rollback. When does it happen? Has it already happened by the time I get in the rescue clause?
I'm using mySQL on Dreamhost and I think they use innoDB.
I've been experimenting with this. It seems like if your rescue catches the exception that would have caused the rollback, the part of the transaction that already happened gets committed. In my case, I want the database rolled back to the way it was before the transaction started, but I still want to handle the exception.
I ended up with this:
self.transaction do
first_operation
begin
operation_that_might_violate_db_constraint
rescue ActiveRecord::RecordNotUnique
#deal with the error
raise ActiveRecord::Rollback #force a rollback
end
end
The raise ActiveRecord::Rollback part makes sure the transaction gets completely rolled back. Without it, the changes from first_operation would end up getting committed.
The ActiveRecord::Rollback is a special kind of exception that doesn't bubble above the level of the transaction, so you won't end up with an uncaught exception that renders the error page.
I'm not sure this is the gold-standard way of doing this, but it seems to work.
Just using begin...rescue isn't enough to rollback a transaction. You need to use:
ModelName.transaction do
end
This is done explicitely on a call to save, so that all of your callbacks are executed together. What exceptions are you catching in your rescue block? What are you responding to? What kind of errors?
Rollback not be processed.
ex:
create_table "helps", :force => true do |t|
t.string "title", :null => false
t.text "content"
end
#Rails console
Help.transaction do
Help.create! title: "aaa"
begin
Help.create! content: "111"
rescue
p "create error."
end
Help.create! title: "bbb"
end
#get this
>> "create error."
Help.count
>> 2
You can also try my answer for rollback, catch and rendering for your create method using ActiveRecord::Base.transaction:-
Click Here
Thanks