Using delayed_job with class methods
A bit of context before going to the solution...
I was trying to call a bulk insertion of some objects in the database in a Rails app hosted in Heroku, taking data from an user uploaded CSV file and processing it via find_or_create_by
for each row, updating all the attributes when necessary and finally persisting the data in the database.
As the data rows grew in number (over 5k entries usually) the timeout limit of 30 seconds imposed by Heroku wasn't enough. So I had to setup the delayed_job gem with the HireFire gem for a cheaper experience in Heroku for the bulk process.
So I did it this way.
# controllers/the_model_controller.rb
TheModel.delay.import_from_csv params[:file] # The offending line
redirect_to root_url, notice: 'Data inserted successfully'
# models/the_model.rb
class TheModel
def self.import_from_csv(file)
CSV.foreach(file_path, headers: true) do |row|
temp_stuff = row.to_hash
the_model = TheModel.find_or_create_by(:attribute1 => temp_stuff['attribute1'])
# ... fill with data
the_model.save!
end
end
end
With the delayed_job ruby gem I was always getting undefined_method
errors.
Routing Error undefined method `import_from_csv' for class `TheModel'
Of course, it is right. You need to have an already existing (and persisted) object for delayed_job to work this way.
The workaround I found is pretty simple actually, though it is not very well documented IMO. You just have to create a singleton version of your class and mark the asynchronous methods with handle_asynchronously :your_async_method
.
Above example, fixed:
# controllers/the_model_controller.rb
TheModel.import_from_csv params[:file]
# Without 'delay' now
redirect_to root_url, notice: 'Data inserted successfully'
# models/the_model.rb
class TheModel
class << self
def from_csv(file_path)
CSV.foreach(file_path, headers: true) do |row|
temp_stuff = row.to_hash
the_model = TheModel.find_or_create_by(:attribute1 => temp_stuff['attribute1'])
# ... fill with data
the_model.save!
end
end
handle_asynchronously :from_csv
end # My importer as a class method
def self.import_from_csv(file)
TheModel.from_csv file.path
end
end
And now it works nicely. Also, only one insertion to the Delayed::Job queue database.