Best way to recover from a failed model save in rails - ruby-on-rails

I have some code to get some tweets from the twitter API:
initial_tweets = get_tweets_in_time_range self.name, (Time.now-1.weeks), Time.now
initial_tweets.each do |tweet|
new_tweet = Tweet.new
new_tweet.favorite_count = tweet.favorite_count
new_tweet.filter_level = tweet.filter_level
new_tweet.retweet_count = tweet.retweet_count
new_tweet.text = tweet.text
new_tweet.tweeted_at = tweet.created_at
new_tweet.created_at = DateTime.strptime tweet.created_at.to_s, '%Y-%m-%d %H:%M:%S %z'
new_tweet.save
# What happens on a failed save
end
Whats the correct fallback if that save fails? As pointed out where the comment is. Thanks for any help.

save only return true or false, you can use save!, it will raise an exception if the record is invalid. if exception raised, you can catch it.
begin
....
new_tweet.save!
rescue exception => e
puts e.inspect
#you can continue the loop or exit
end
as #Stefan said, you can wrap you code in a transaction, if one record save failed, all saved record will rollback. I don't advise you to do this unless you really want every record saved success.
Tweet.transaction do
initial_tweets = get_tweets_in_time_range self.name, (Time.now-1.weeks), Time.now
initial_tweets.each do |tweet|
new_tweet = Tweet.new
.....
new_tweet.created_at = DateTime.strptime tweet.created_at.to_s, '%Y-%m-%d %H:%M:%S %z'
new_tweet.save! # you have to add '!', once save failed, it will trigger rolls back.
end
end

Related

How to handle multiple exceptions in a Transaction with Ruby on Rails for importing a text file

My ruby environment is: Ruby 2.3.1 and Rails 5.0.0.1.
I'm trying to import a text file for import a lot of purchases items.
Example of purchase file:
data.txt
Customer\tDescription\tUnit Price\tQuantity\tAddress\tSupply company\n
Athos Matthew\tChocolate\t10\t5\tSome address.\tChocolate company\n
The columns are divided by a tab (\t) and it has an enter at the final (\n).
I have the purchase class where all attributes cannot be null. The attributes are:
custumer_name:string
product_id:integer # It has relationship with the Product Resource
product_quantity:integer
supply_company_id:integer # It has relationship with the SupplyCompany Resource
To import the file, I decided to create a PurchaseImporter class to do this job and keep the code cleaner.
My problem is that transaction part:
begin
ActiveRecord::Base.transaction do
purchase = Purchase.new
data = line.force_encoding('UTF-8').split(/\t/)
purchase.customer_name = data[0]
product = Product.find_or_create_by!(description: data[1], price: data[2])
purchase.product_quantity = data[3]
purchase.product = product
supply_company = SupplyCompany.find_or_create_by!(name: data[5], address: data[4])
purchase.supply_company = supply_company
purchase.save!
end
rescue Exception => e
#errors[:import][index] = e.message
end
My problem is that I want to catch all the raised errors from the Product, SupplyCompany and Purchase that could happen inside this transaction.
This is the order of the happenings without the unnecessary code to explain it.
product = Product.find_or_create_by!(description: data[1], price: data[2])
supply_company = SupplyCompany.find_or_create_by!(name: data[5], address: data[4])
purchase.save!
I need to print this errors information to this 3 classes in the screen, but with my code, I can only catch the first exception error, generated by the product. If some error happened in the SupplyCompany or in the Purchase, I lost these errors messages.
Are there other ways to import and log the errors message when importing files?
You can have more specific exception handling... do a rescue for each section you want to trap, at the end raise an error if any previous error was encountered (to get you out of the transaction block) and test in that final error that you're rescuing your own raise otherwise it's some other problem and you need to halt.
begin
ActiveRecord::Base.transaction do
error_encountered = false
purchase = Purchase.new
data = line.force_encoding('UTF-8').split(/\t/)
purchase.customer_name = data[0]
begin
product = Product.find_or_create_by!(description: data[1], price: data[2])
purchase.product_quantity = data[3]
purchase.product = product
rescue Exception => e
#errors[:import][index] = e.message
error_encountered = true
end
begin
supply_company = SupplyCompany.find_or_create_by!(name: data[5], address: data[4])
purchase.supply_company = supply_company
rescue Exception => e
#errors[:import][index] = e.message
error_encountered = true
end
begin
purchase.save!
rescue Exception => e
#errors[:import][index] = e.message
error_encountered = true
end
raise 'break out of transaction' if error_encountered
end
rescue Exception => e
raise unless e.message == 'break out of transaction'
end
Since you're rescuing Exception it's hard to know what error is actually emerging. When rescuing, you should try to use a more specific class when possible.
You also might not need to use rescue at all. The active methods you're using: find_or_create_by! and save! can be written without the exclamation point so that they don't raise errors.
In active record, if you try and save something with validation errors then the <record>.errors.full_messages array is populated. It won't necessarily raise an error if you don't use the exclamation point (although errors can be raised from all sorts of things regardless).
So, for example you can try and save a record and check for errors like this:
product = Product.find_or_initialize_by(description: data[1], price: data[2])
product.save
errors[:import][index] ||= []
errors[:import][index].concat product.errors_full_messages
Actually, in this case I think your approach makes some sense. You're saving a few records in sequence. If the first fails, then probably the others will fail - so is it worth even attempting to save those subsequent records? I'll let you decide.

Ruby on Rails: How to handle exceptions(rescue?)

I'm fetching videos' data from youtube, including general video info, and likes number of them.
The problem is that sometimes, there is no corresponding data. Maybe the video is private, or the likes number is 0.
In either case, there will report NoMethodError: undefined method 'likes' for nil:NilClass
How can I handle this? I just want to ignore them and continue to the next youtube video.
I think I can use something like rescue, but I don't find many tutorial of it.
Here is my code:
client = YouTubeIt::Client.new(:dev_key => my_key)
Video.all.each do |video|
video_info = client.video_by(video.url)
like_number = video_info.rating.likes.to_i
video.likes = like_number
if video.save
puts "#{video.title} likes: #{video.likes}"
end
end
Rather that using the exception handling, which is slower, in this case just check for the presence of the value before calling the method.
Assuming the error is generated on the line
like_number = video_info.rating.likes.to_i
simply use
client = YouTubeIt::Client.new(:dev_key => my_key)
Video.all.each do |video|
video_info = client.video_by(video.url)
# here check for the presence.
if video_info.rating
like_number = video_info.rating.likes.to_i
video.likes = like_number
if video.save
puts "#{video.title} likes: #{video.likes}"
end
end
end
You should check for all possible nil values with, eg, video.nil? and corresponding conditionals. Rescuing should always be your last resort (it's slow and dirty), but, if there is no way to predict where the script will fail, you can always rescue some piece of code:
begin
#your error-prone code
rescue
#action you want to take in case your error-prone code raises error
end
Here is a good article on exceptions: http://www.tutorialspoint.com/ruby/ruby_exceptions.htm.
In this example, the method body acts as default begin block so need of begin block
def any_method
client = YouTubeIt::Client.new(:dev_key => my_key)
Video.all.each do |video|
video_info = client.video_by(video.url)
# try tries to find the method, if not found returns nil
like_number = video_info.try(:rating).try(:likes).to_i
video.likes = like_number
if video.save
puts "#{video.title} likes: #{video.likes}"
end
end
# For any type of standard errors, use rescue block
rescue => error_object
p 'Sorry, some error occured'
p error_object
end

Ruby on Rails How to treat a error from a method call

I have this ruby code:
def get_last_quote(ticker)
todays_date = Date.today
data = YahooFinance::Scraper::Company.new(ticker.downcase).historical_prices(todays_date, todays_date)
return data.first[:close]
end
When today's date is Sunday or Saturday I don't want any data, because markets are closed. Same thing happens on holidays or for any other day when the markets are closed.
So if it fails I want to subtract 1 from the days and check again, until I find a valid day.
The problem is that when the day is not valid I get Ruby on Rails error and I don't know how to treat it.
I tried:
while data.nil?
But it does not work, the error happens when I try to attribute the result to data, so I don't have a chance to check whether data is valid or not.
Any ideas, is it possible?
use begin then rescue ErrorClass with finally end
example
def method(args)
args += 1
end
def call_method
begin
method(92929292)
rescue ArgumentError, e
e.message
end
end

As would be appropriate to make an exception in the model?

I have a method parse_date_if_not_null which parses the date and time. But it so happens that the user entered an incorrect date format and time, then you need to show the error. I implemented it this way.
But I think, catch here only wrong format exception.
As would be appropriate to make an exception?
def parse_date_if_not_null
unless self.date_string.blank?
begin
self.ends_at = DateTime.strptime self.date_string, '%m/%d/%Y %H:%M'
rescue
errors.add(:date_string, _("Wrong date format, example: MM/DD/YYYY HH/MM"))
end
end
end
Yes it will be appropriate to make an exception in the model, you can do something like this..
class ...
validate: parse_data_if_not_null
def parse_data_if_not_null
unless self.date_string.blank?
erros.add(:date_string, 'Wrong date format, example: ...') if ((self.ends_at = DateTime.strptime self.date_string, '%m/%d/%Y %H:%M') rescue ArgumentError) == ArgumentError)
end
end

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