DEV Community

Cover image for Export Records to CSV Files with Rails
Victor Hazbun
Victor Hazbun

Posted on • Updated on

Export Records to CSV Files with Rails

Learn how to export records into CSV files using Ruby on Rails. Here are the steps.

Steps:

  1. Add a controller and make sure you handle the csv request.
  2. Add a route to point to your controller
  3. Add a model with the class method to_csv

Lets code it!

Controller

# users_controller.rb

class UsersController < ApplicationController
  def index
    @users = User.all

    respond_to do |format|
      format.html
      format.csv { send_data @users.to_csv, filename: "users-#{Date.today}.csv" }
    end
  end
Enter fullscreen mode Exit fullscreen mode

Routes

# routes.rb

...

resources :users, only: :index

...
Enter fullscreen mode Exit fullscreen mode

Model

# user.rb

class User < ActiveRecord::Base
  def self.to_csv
    attributes = %w{id email name}

    CSV.generate(headers: true) do |csv|
      csv << attributes

      all.find_each do |user|
        csv << attributes.map{ |attr| user.send(attr) }
      end
    end
  end

  def name
    "#{first_name} #{last_name}"
  end
end
Enter fullscreen mode Exit fullscreen mode

Core custom methods:

to_csv will map out the "id, email, name" values of your model collection and then generate a CSV string.


Rails core methods for this feature:

#send_data Sends the given binary data to the browser. This method is similar to render :text => data, but also allows you to specify whether the browser should display the response as a file attachment (i.e. in a download dialog) or as inline data. You may also set the content type, the apparent file name, and other things. Source

Discussion (11)

Collapse
haroldus profile image
haroldus-

How about doing this as a concern instead?

# models/concerns/generate_csv.rb
module GenerateCSV
  extend ActiveSupport::Concern

  class_methods do
    def generate_csv
      CSV.generate(headers: true) do |csv|
        csv << self.attribute_names

        all.each do |record|
          csv << record.attributes.values
        end
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Then

# models/user.rb
class User
  include GenerateCSV
end
Enter fullscreen mode Exit fullscreen mode

or

# models/whatever.rb
class Whatever
 include GenerateCSV
end
Enter fullscreen mode Exit fullscreen mode
Collapse
countalucard2022 profile image
countalucard2022

Great for DRY approach, thanks for this :)

Collapse
tomk32 profile image
Thomas R. Koll • Edited

Nitpicking if I may, a missing last or first name won't be noticed in HTML but it will be noticed in CSV when a human reads it directly or imported in Excel/LibreCalc.

def name
  [first_name, last_name].compact.join(' ')
end
Collapse
nuno101 profile image
Nuno

Nice, but there's a mistake in the controller/ model. The code appears to work because @users=User.all which matches the set of users computed within the model class

The correct call would be User.to_csv(@users). The User model function has to be modified to take the new parameter into account.

Collapse
victorhazbun profile image
Victor Hazbun Author

nope, it's right the way it is. Since @users is an ActiveRecord::Relation so you can say @users.to_csv

Collapse
von_christian profile image
Von Christian

How to run it in background job(Sidekiq) ?

Collapse
victorhazbun profile image
Victor Hazbun Author

It depends, are you going to send an email on a background job? or are you going to upload the file on S3? please give some context.

Collapse
von_christian profile image
Von Christian

from my experience, when large amounts of data is involved, the server times out. Im thinking if it is possible to move the CSV export and show a download link to the user?

Thread Thread
victorhazbun profile image
Victor Hazbun Author

Ok, then you will need to do the following:

  1. On a BG job generate the CSV file
  2. Upload the file to AWS S3
  3. Save the URL in the DB
  4. Expose the URL in the front-end
Collapse
victorhazbun profile image
Victor Hazbun Author

UPDATE: I was using #each, I changed it to#find_each which saves memory. See api.rubyonrails.org/classes/Active...

Collapse
countalucard2022 profile image
countalucard2022

But this will not be a report friendly, find_each forces the sequence to be order by id desc :( thanks to the postoverall!