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.