Nested transactions and rollbacks in rails - ruby-on-rails

My question is in regards to using nested transactions in rails with ActiveRecord.
What I need to do is make sure both saves are successful else rollback if ether fail and return true or false based on the success.
Hers is the code
Card.transaction do
Transaction.transaction do
#card_saved = card.save
#transaction_saved = self.save
end
raise ActiveRecord::Rollback
end
if #transaction_saved and #card_saved
return true
end
return false
Do I need to add a rollback in the inner block as well or will the outer catch both?

Related

Modifying ActiveRecord models before preventing deletion

Some records in my application have a DOI assigned to them and in that case they should not be deleted. Instead, they should have their description changed and be flagged when a user triggers their deletion. A way to do this, I thought, would be as follows in the relevant model:
before_destroy :destroy_validation
private
def destroy_validation
if metadata['doi'].blank?
# Delete as normal...
nil
else
# This is a JSON field.
modified_metadata = Marshal.load(Marshal.dump(metadata))
description = "Record does not exist anymore: #{name}. The record with identifier content #{doi} was invalid."
modified_metadata['description'] = description
modified_metadata['tombstone'] = true
update_column :metadata, modified_metadata
raise ActiveRecord::RecordNotDestroyed, 'Records with DOIs cannot be deleted'
end
end
This does indeed prevent deletion, but the record appears unchanged afterwards rather than having a modified description. Here's an example of a test:
test "records with dois are not deleted" do
record = Record.new(metadata: metadata)
record.metadata['doi'] = 'this_is_a_doi'
assert record.save
assert_raises(ActiveRecord::RecordNotDestroyed) { record.destroy! }
assert Record.exists?(record.id)
modified_record = Record.find(record.id)
puts "#{record.description}" # This is correctly modified as per the callback code.
puts "#{modified_record.description}" # This is the same as when the record was created.
end
I can only guess that Rails is rolling back the update_column due to an exception having been raised, though I may be mistaken. Is there anything I can do to prevent this?
save and destroy are automatically wrapped in a transaction
https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
So destroy fails, transactions is rolled back and you can't see updated column in tests.
You could try with after_rollback callback https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_rollback
or do record.destroy check for record.errors, if found update record with method manually record.update_doi if record.errors.any?.
before_destroy :destroyable?
...
def destroyable?
unless metadata['doi'].blank?
errors.add('Doi is not empty.')
throw :abort
end
end
def update_doi
modified_metadata = Marshal.load(Marshal.dump(metadata))
description = "Record does not exist anymore: #{name}. The record with identifier content #{doi} was invalid."
modified_metadata['description'] = description
modified_metadata['tombstone'] = true
update_column :metadata, modified_metadata
end
Tip: use record.reload instead of Record.find(record.id).

Possible bad code or Rails multi-thread issue

I'm having an issue with the below code frequently in a Rails 4.4, Ruby 2.2 with puma web server application. This code has excessive if statements just to try and figure out the problem.
We can assume that assign_coupon() returns true or false. When it returns true it creates an association between the review and the coupon, so you can type review.coupon. Before you run assign_coupon() or if assign_coupon() returns false review.coupon will be nil.
The issue is that assign_coupon() is not working and review.coupon is nil, yet the code gets past 'if CouponCodeService.assign_coupon(review)' and then also passes 'if !review.coupon.nil?' and is running 'review.approve!'.
I cannot figure out for the life of me how the code gets passed 2 failing if statements to run 'review.approve!'. This should not be possible.
if review.product.has_coupon_codes?
if CouponCodeService.assign_coupon(review) # this is false
if !review.coupon.nil? # this is false
review.approve! # this is some how executed.
success_message(review)
review.send_reviewer_approved_email
else
review.update(aasm_state: 'requested')
error_message(review)
end
else
review.update(aasm_state: 'requested')
error_message(review)
end
end
def self.assign_coupon(review)
begin
product = review.product
if product.has_coupon_codes?
coupon = product.coupons.where(claimed: false).first
coupon.review_id = review.id
coupon.claimed = true
if coupon.save
return true
else
return false
end
else
return false
end
rescue Exception => e
return false
end
end

Rollback in Rails?

In my Rails application, I have a method which copies many rows, and also goes on to copy down some of the parent-child relationships.
def merge
params[:merge_rows].each do |merge_row|
batch_detail = BatchDetail.find(merge_row)
batch_detail.duplicate
batch_detail.batch_id = batch.id
batch_detail.save
end
render nothing: true
end
# BatchDetail.duplicate
def duplicate
batch_detail = dup
batch_detail.primer3_parameter = primer3_parameter.dup if primer3_parameter.present?
primer3_outputs.each do |primer3_output|
batch_detail.primer3_outputs << primer3_output.duplicate
end
batch_detail
end
Ideally, I would like to only save if all rows are successfully duplicated, and rollback all changes if any are unsuccessful.
Then I would like to report 200 or 500 via the render if successful or error.
wrap your ActiveRecord changes in a transaction block, if the end of the block is bypassed by some exception, all changes are rolled back.
begin
ActiveRecord::Base.transaction do
...various transactions
if (some_error_condition)
raise
end
end
...stuff to do if all successful
rescue
...stuff to do on failure
end

Is there a more rubylike way of doing this helper function

def work_location(application)
if application.contact.work_location.blank? rescue nil
return false
else
return true
end
return false
end
Basically i want to return true or false ....I only want to return true if the work_location is not blank and i need to catch the nil error
Actually this produces a syntax error
syntax error, unexpected modifier_rescue, expecting keyword_then or ';' or '\n'
..._location.blank? rescue nil
def work_location(application)
application.try(:contact).try(:work_location).present?
end
Personally I dislike handling potential nils by doing rescue false because you catch far more than nils: such a rescue rescues all sorts of other errors, for example it will catch NoMethodError, so if you'd typed one of the method names it would squash that error and make it much harder to track down.
Write tests and check both true and false return cases
Shorten code above with:
def work_location(application)
application.contact.work_location.blank? rescue true
end
As far as I can tell, you are creating a helper method here.
I should define a method on application, which you can then use in your views.
The advantage: it is purely object-oriented. An application should know if it has a workplace or not.
Secondly, use try: it will only attempt the given method or block if the receiver is not nil, else it returns nil.
So :
class Application
def has_work_location?
self.contact.try { |c| c.work_location.present? }
end
end
Note that this usage of try only works in rails 3.2, if you are on an older version it does not accept a block. Furthermore nil.present? works and returns falso, so you could write
def has_work_location?
self.contact.try(:work_location).present?
end
Note: because we are adding a method to application, we can safely assume application, so we only need to check that the contact exists anymore.
In your views you can then just write:
<%= #application.contact.workplace if #application.has_work_place? %>
or something similar. Hope this helps.

save an active records array

I have an array like this
a = []
a << B.new(:name => "c")
a << B.new(:name => "s")
a << B.new(:name => "e")
a << B.new(:name => "t")
How i can save it at once?
B.transaction do
a.each(&:save!)
end
This will create a transaction that loops through each element of the array and calls element.save on it.
You can read about ActiveRecord Transactions and the each method in the Rails and Ruby APIs.
a.each(&:save)
This will call B#save on each item in the array.
So I think we need a middle ground to Alexey's raising exceptions and aborting the transaction and Jordan's one-liner solution. May I propose:
B.transaction do
success = a.map(&:save)
unless success.all?
errored = a.select{|b| !b.errors.blank?}
# do something with the errored values
raise ActiveRecord::Rollback
end
end
This will give you a bit of both worlds: a transaction with rollback, knowledge of which records failed and even gives you access to the validation errors therein.
Wrapping save in transaction will not be enough: if a validation is not passed, there will be no exception raised and no rollback triggered.
I can suggest this:
B.transaction do
a.each do |o|
raise ActiveRecord::Rollback unless o.save
end
end
Just doing B.transaction do a.each(&:save!) end is not an option either, because the transaction block will not rescue any exception other than ActiveRecord::Rollback, and the application would crash on failed validation.
I do not know how to check afterwards if the records have been saved.
Update. As someone has downrated my answer, i assume that the person was looking for a cut-and-paste solution :), so here is some (ugly :)) way to process fail/success value:
save_failed = nil
B.transaction do
a.each do |o|
unless o.save
save_failed = true
raise ActiveRecord::Rollback
end
end
end
if save_failed
# ...
else
# ...
end
I know this is an old question but I'm suprised no one thought of this:
B.transaction do
broken = a.reject { |o| o.save }
raise ActiveRecord::Rollback if broken.present?
end
if broken.present?
# error message
end
In case you're looking for more efficient solution than save each row in the loop please look my answer here Ruby on Rails - Import Data from a CSV file
I'm suggesting to use gem activerecord-import there.

Resources