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.
Related
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/
I have a method similar to this:
def create
reservation = Reservation.create(params[:reservation_params])
if reservation.valid?
reserved_hour = ReservedHour.create(params[:reserved_hour_params])
if reserved_hour.valid?
notification = Notification.create(params[:notification])
if !notification.valid?
reservation.destroy
reserved_hour.destroy
end
else
reservation.destroy
end
end
end
Now I'd like to test database fail cases with RSpec. For example I'd like to simulate database crash during notification creating and test if reservation and reserved_hour destroy successfully. Is there some way to do this without expanding my create method for test purposes only? I can simulate crash for all three cases by running ActiveRecord::Base.remove_connection, but I have no idea how could I test the case with a single crash.
Your code isn't going to work because all of your .create calls will always return something (either a saved record or an unsaved record) and your if statements will always be true.
Why not use .create! (which will raise an error if create is unsuccessful) within a transaction. Something like:
def create
ActiveRecord::Base.transaction do
begin
Reservation.create!(params[:reservation_params])
ReservedHour.create!(params[:reserved_hour_params])
Notification.create!(params[:notification])
rescue SomeError =>
# do something with SomeError
end
end
end
That way, your transactions will be rolled back if you have an error and you don't have to do all that .destroy business.
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
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.
I'm writing an import routine that will allow a user to upload a CSV file to load their database. Each row of the CSV corresponds to a model.
I'm using FasterCSV to read the file and split the data into individual models, which is working great. I'm just having trouble deciding on the best approach to handle errors.
Right now I have this going, but it really seems wrong to me:
def import(collection)
begin
self.transaction do
collection.collect{|object| object.save!}
end
rescue ActiveRecord::RecordInvalid => invalid
return false
end
return true
end
Is there a better way to save a collection of models?
Using an exception in a loop is going to cause all kinds of headaches for you when you want to track down the problematic record. What you might do is try and save them all, but report on those with errors:
def import(collection)
failed = nil
transaction do
failed = collection.reject { |r| r.save }
unless (failed.empty?)
raise ActiveRecord::Rollback
end
end
failed
end
This presumes you're interested in having a look at the errors. If any records fail, they will be returned in an Array. Otherwise you'll get a nil, which means no errors.
If you don't care, you can always just do a quick and dirty save:
def import(collection)
transaction do
collection.each(&:save!)
end
end
This will pop an ActiveRecord::RecordInvalid exception for the first failure.
For this problem I think the better approach is yours. Maybe not all the records in the csv aré going yo validare, and those that not doesnt matter (log them in order to know the errors)