In a previous article we showed you how to export your data to a CSV file format. What if we want to import a CSV file provided by the user? This article will show you how.

In this example, lets say we have a list of products. Each product has a name, quantity, and price. Run the following command in a new or existing project to create this model.

Terminal Commands:

rails g model Product name:string quantity:integer price:decimal{12,2}
rake db:migrate

This model will view and store our product information. Now we need to create a place that will allow the user to upload a CSV file. lets create a new controller for our products. Run the following command from a rails terminal:

Terminal Commands:

rails g controller Products index import

Great, now lets modify our routes file. Open your routes file and modify it to look like the following code making sure to keep your own application name instead of CSVImportExample:

config/routes.rb:

CSVImportExample::Application.routes.draw do
  resources :products do
    collection { post :import }
  end

  root to: "products#index"
end

Now, lets modify our model to accept the CSV and process it. Open up the products model and modify it to look like the code below:

app/models/product.rb

class Product < ActiveRecord::Base
  require 'csv'

  def self.import(file)
    CSV.foreach(file.path, headers: true) do |row|

      product_hash = row.to_hash # exclude the price field
      product = Product.where(id: product_hash["id"])

      if product.count == 1
        product.first.update_attributes(product_hash)
      else
        Product.create!(product_hash)
      end # end if !product.nil?
    end # end CSV.foreach
  end # end self.import(file)
end # end class

The code above will check to see if the product already exists in the database. if it does, it will then attempt to update the existing product. If not, it will attempt to create a new product.

Now, lets edit our controller and add the necessary code to call this function. Open up your products controller and modify it to look like the code shown below.


class ProductsController < ApplicationController
  def index
    @products = Product.all
  end

  def import
    Product.import(params[:file])
    redirect_to root_url, notice: "Products imported."
  end
end

Okay, now lets edit our products index view to display a list of products and provide a form that allows the user to upload the CSV. Open up your products/index.html.erb view and modify it to look like the following code:

app/views/products/index.html.erb:

<%= flash[:notice] %>
<table>
  <thead>
    <tr>
      <th>Id</th>
      <th>Name</th>
      <th>Price</th>
      <th>Quantity</th>
    </tr>
  </thead>
  <tbody>
    <% @products.each do |product| %>
      <tr>
        <td><%= product.id %></td>
        <td><%= product.name %></td>
        <td><%= product.price %></td>
        <td><%= product.quantity %></td>
      </tr>
    <% end %>
  </tbody>
</table>
<div>
<h3>Import a CSV File</h3>
  <%= form_tag import_products_path, multipart: true do %>
    <%= file_field_tag :file %>
    <%= submit_tag "Import CSV" %>
  <% end %>
</div>

Excellent! Now if we start a rails server and navigate to http://localhost:3000 we will see a place to upload the csv. Copy paste the following lines into a file and try to upload it.

products.csv

id,name,quantity,price
1,Guitar,20,199.99
2,Trumpet,5,299.99
3,Piano,3,699.99
4,Clarinet,10,59.99

Excellent! You will notice that the products were imported successfully. Try and change up the CSV, adding and changing the different rows and fields. You'll notice that the records automatically update.

What if we want to exclude certain fields from the update? Rails makes this easy. Lets say for example that we want to exclude the price field from the update. Open up your products model and change it to look like the code listed below.


class Product < ActiveRecord::Base
  require 'CSV'

  def self.import(file)
    CSV.foreach(file.path, headers: true) do |row|

      product_hash = row.to_hash # exclude the price field
      product = Product.where(id: product_hash["id"])

      if product.count == 1
        product.first.update_attributes(product_hash.except("price"))
      else
        Product.create!(product_hash)
      end # end if !product.nil?
    end # end CSV.foreach
  end # end self.import(file)
end # end class

If you change the prices in the products.csv file you'll notice that it doesn't affect the data stored in your rails application.

What about validating the CSV file? An extremely simple way to handle that would be to wrap the import call in your controller in a begin..rescue..end block. For example, modify your products controller to look like the code listed below.


class ProductsController < ApplicationController
  def index
    @products = Product.all
  end

  def import
    begin
      Product.import(params[:file])
      redirect_to root_url, notice: "Products imported."
    rescue
      redirect_to root_url, notice: "Invalid CSV file format."
    end
  end
end

Now when you try to upload an invalid CSV file you will receive an error.

That's it! We hope you enjoyed this article. Feel free to download the example code included and play around with it. Please leave any suggestions, questions, or concerns in the comment section below.