ActiveRecord::Base.transaction do
do_this
Something.after_commit.action
do_that
end
# Something.action is fired/run in case no exceptions in transaction
How can one achieve this?
NOTE: one doesn't see where the transaction starts and ends (I mean transaction do and end)
As I understand your question you want to define code that should be run after the transaction inside a transation? But you do not want to run it straight away?
If this is the case it can be achieved with a proc. Info about procs.
So the code would be something like:
no_exceptions = true
ActiveRecord::Base.transaction do
do_this
after_code = Proc.new do |no_e|
# Code defining how to act no_e gives you info if exceptions where fired.
end
do_that
rescue Exception => e
no_exceptions = false
end
after_code.call(no_exceptions)
I hope this was what you where looking for.
You must use this pattern, with the save! (that raises exception on error):
begin
do_this_before_update_the_db
ActiveRecord::Base.transaction do
update_1_db_if_no_fails
update_2_db_if_no_fails
...
update_n_db_if_no_fails
end
DO-THIS-AFTER-COMMIT-ONLY-IF-NO-EXCEPTIONS
rescue Exception => exception
do_this_if_some_update_fails # depending on the exception you must raise again from here.
ensure
do_this_always_fail_or_no # this runs after commit
end
You can't put after_commit inside the transaction, because this run inside the transaction, before the commit is done.
All that is inside the transaction block runs with one commit at the end of the block. All callbacks runs inside this transaction. And all indented transactions runs inside this transaction (with only one commit). You can found detailed information here.
You must see that at the log, with only one commit at the end.
Also you can have this pattern on update_2_db_if_no_fails, with an ensure block that runs always fail or no the update_2. This run inside the first transaction, but is something different of your model after_commit callback.
Related
I am using transaction block and doing some operation inside the block, there are certain things that should ignore transaction block, i can't move the update from the transaction block as it is tightly couple with code.
How can we skip certain portion and let the rest behaviour as usual
Record.transaction do
check_for_errors required_columns
create_report
end
def check_for_errors
loop
...
begin
Methods
// want to skip this perticular db update from transaction block
job.update_column(total_number: loop index)
// as this is under transaction block no changes can be seen on ui
rescue => e
populate_error_message(e.message)
raise ActiveRecord::Rollback
end
end
end
Any idea what can be done in this case?
To skip a particular db action within an existing transaction, you can execute it in a separate thread. E.g.for your case above
...
Thread.new do
ActiveRecord::Base.connection_pool.with_connection do
job.update_column(total_number: loop_index)
end
end.join
... # continue method
model1.rb
def method1
Model1.transaction do
model2_ref_obj = Model2.find(some_id)
model2_ref_obj.method1
end
end
model2.rb
def method1
Model2.transaction do
## so some work
self.save!
end
end
However, due to some issue, model1's transaction rollback, will inner transaction will also roll-back.
If the error occurs in the second transactional block, the error is effectively rescued by that block, meaning the first transaction block thinks everything is hunky dory, goes ahead and commits the transactions - including those that should be rolled back in the second transactional block.
You have to be very careful when nesting transactions. The short answer is it varies depending on how the nesting is structured. Some good reading:
https://pragtob.wordpress.com/2017/12/12/surprises-with-nested-transactions-rollbacks-and-activerecord/
How can I use rescue to continue a loop. I’ll make an example
def self.execute
Foo.some_scope.each do |foo|
# This calls to an external API, and sometimes can raise an error if the account is not active
App::Client::Sync.new(foo).start!
end
end
So normally rescue Bar::Web::Api::Error => e would go at the end of the method and the loop will stop. If I could update a attribute of the foo that was rescued and call the method again, that foo would not be included in the scope and I would be able to start the loop again. But the issue with that is, I only want this once for each foo. So that way would loop through all of the existing foo again.
What’s another way I could do this? I could make a private method that is called at the top of the execute method. This could Loop through the foo and update the attribute so they aren’t part of the scope. But this sounds like an endless loop.
Does anyone have a good solution to this?
You can put a begin and rescue block within the loop. You talk about "updating an attribute of the foo" but it seems you only want that to ensure this foo is not processed on a restart of the loop, but you don't need to restart the loop.
def self.execute
Foo.some_scope.each do |foo|
# This calls to an external API, and sometimes can raise an error if the account is not active
begin
App::Client::Sync.new(foo).start!
rescue Bar::Web::Api::Error
foo.update(attribute: :new_value) # if you still need this
end
end
end
You could use retry. It will re-execute the whole begin block when called from a rescue block. If you only want it to retry a limited number of times you could use a counter. Something like:
def self.execute
Foo.some_scope.each do |foo|
num_tries = 0
begin
App::Client::Sync.new(foo).start!
rescue
num_tries += 1
retry if num_tries > 1
end
end
end
Documentation here.
I am wrapping some code in a transaction block i.e.
Tip.transaction do
...make changes to lots of tips...
end
The reason I am doing this is I want to make sure all the changes are made before being committed to the database. How do I use rspec to test that if a failure occurs before the transaction is completed, then the database will rollback to its previous state?
You can just check that no Tip was persisted to the in case of failure. The only doubt here is what does a failing tip means for you, since that's what you will reproduce in your test. A plain vanilla example:
# app/models/tip.rb
before_save :fail_if_bad_amoun
def fail_if_bad_amount
fail BadAmount if amount == 'bad'
end
def self.process(tips)
Tip.transaction do
tips.all? &:save
end
end
# spec/models/tip_spec.rb
it "does not commit in case of failure in one of the tips" do
good_tip = Tip.new(amount: 1_000)
bad_tip = Tip.new(amount: 'bad')
expect do
Tip.process([good_tip, bad_tip])
end.not_to change { Tip.count }
end
So I am trying to achieve atomicitiy in while doing my saves (which are essentially updates on each rows).
params[:player_types].each do |p_type_params|
if p_type_params[:id]
player = #player_types.find(p_type_params[:id])
player.assign_attributes(p_type_params)
#player_types << player
end
end
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
if player_type.save
"DO Something.."
else
"DO something else.."
errors = true
end
end
end
Inspite of saving withing the transaction block, I can see partial saves also i.e. one of the rows get updated and the erroneous one does not (obviously) where as I would have wanted the updated row to be rolled back since there was atleast one row that could not be updated due to an error. Is my interpretation of transaction block correct in this case? How do I achieve an atomic save in my case?
EDIT: The model validates for uniqueness of one of the columns, which would be the reason for failing to update in the Database at this point.
You need to raise an error inside your transaction block to abort the transaction; setting errors doesn't impact the transaction.
For instance:
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
if player_type.save
"DO Something.."
else
"DO something else.."
raise "save failed!"
end
end
end
The more conventional way of doing this, of course, is to use save! which raises an exception for you when it fails:
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
player_type.save!
end
end
If you really need to "DO Something" on failure (besides aborting the transaction), you'll have to use the first method.