Importing from CSV in Rails

Building a CSV import option into your application can be very helpful for getting a lot of records into your database in one step. While building out some of our recent reporting additions at Rigor, I wanted to include the option to import CSV data to help our team migrate records from one service to another.

Using Ruby’s standard CSV library makes reading CSV files a breeze. Implementing the import function in a Rails-like way, however, can be more difficult. In general, Rails controller actions look like:

def index
	@my_model = MyModel.new(params[:my_model])
	if @my_model.save
		# yay, it worked! render some success page
	else 
		# womp, render some helpful error messages
	end
end

Massaging a CSV import into a controller action of this format isn’t too terribly difficult, but it may not be completely obvious at first. For my import, I opted to lean on the ActiveModel::Model module (say that five times fast) in Rails 4 to create a model-like wrapper around the CSV import functionality.

I started by creating a class in my models directory and including the module:

class MyAwesomeImporter
	include ActiveModel::Model
end

To get the Rails model-like behavior, we have to define a few model methods:

class MyAwesomeImporter
	include ActiveModel::Model
	def persisted?
		false # since this model isn't ever persisted, just return false
	end
	
	def valid?
		# logic to determine if import is valid
	end
end

For the valid? method, define what a valid import should look like and test it there, returning true or false. For example:

def valid?  record_attributes = read_stuff_from_csv  import_records = record_attributes.map {|attrs| MyModel.new(attrs)}  import_records.map(&:valid?).all?end

With the model-y parts out of the way, now we just have to set up our model with access to the CSV file and define read_stuff_from_csv and then we can use our new class in a controller action just like we always do:

# my_awesome_importer.rbdef initialize(file)  @file = fileenddef read_stuff_from_csv  CSV.new(@file, headers: :first_row).each do |row|    # do stuff with the row data  end  # return some useful data for making recordsend
# somewhere in my controllerdef import  @my_awesome_importer = MyAwesomeImporter.new(params[:csv_file])  if @my_awesome_importer.save    # let the user know the import worked  else    # boo, return some errors  endend

In addition to being easy to read and understand, using a symbolic model to wrap our import makes it easy to add helpful errors to users when an import fails. Since we included the ActiveModel::Model, adding validation errors is simple. For example, if we want to make sure the imported CSV isn’t empty, we can just add the following:

def csv_empty?  if CSV.new(File.open(file2), headers: :first_row).to_a.empty?    # add a helpful error message for the user    errors.add :base, "CSV is empty"    true  endend

Then we can update our valid? definition to check if the file is empty first:

def valid?  return false if csv_empty?  # original validationsend

By leveraging Rails ActiveModel::Model module, we can incorporate the helpful features of a model into our simple Ruby class. When working with objects that span the MVC pattern, consider creating a model-like object to keep your controllers Railsy and keep form error-handling simple.