Export CSV from Rails
In a previous article we exported data from a Rails database to XLSX using a gem.
We can export to CSV without any external gems, because Ruby has an in-built CSV processor.
I see two good approaches to generating CSV:
1. *.csv.erb
template without Ruby::CSV
(elementary approach) #
A rails app can respond to format csv by default.
We can create a template with some data formatted as CSV lines and have a link to download the rendered page.
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.all
respond_to do |format|
format.html
format.csv
end
end
# app/views/users/index.hmtl.erb
<%= link_to "CSV export", users_path(format: :csv), download: ['Users', Date.today].join(' ') %>
# app/views/users/index.csv.erb
# send all fields
<% fields = [:id, :email, :last_name] %>
<%= fields.map(&:to_s).join(";") %>
<% @users.each do |user| %>
<%= fields.map { |field| user[field].to_s }.join(";") %>
<% end %>
# or send selected fields
<%= User.column_names.map(&:to_sym).join(";") %>
<% @users.each do |user| %>
<%= user.attributes.values_at(*User.column_names).join(";") %>
<% end %>
This approach requires quite a lot of data transformation.
2. *.csv.erb
template with Ruby::CSV
(more correct approach) #
Instead of transforming data in the above template, we can let Ruby::CSV
handle the generation of correctly formatted CSV lines.
# app/views/users/index.hmtl.erb
<%= link_to 'Export Users', users_path(format: :csv) %>
# app/controllers/users_controller.rb
class UsersController < ApplicationController
require 'csv'
def index
@users = User.all
respond_to do |format|
format.html
format.csv do
filename = ['Users', Date.today].join(' ')
response.headers['Content-Type'] = 'text/csv'
response.headers['Content-Disposition'] = "attachment; filename=#{filename}.csv"
render template: 'users/index'
end
end
end
# app/views/users/index.csv.erb
<%- headers = ['Last Name', 'Email'] -%>
<%= CSV.generate_line headers %>
<%- @users.each do |user| -%>
<%= CSV.generate_line([user.last_name, user.email]) -%>
<%- end -%>
This approach is more pure/less hacky.
3. Without a template (more generic approach) #
Use
[send_file
method](https://api.rubyonrails.org/v6.1.4/classes/ActionController/DataStreaming.html]
to assign user attributes that should be present in the generated CSV:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
require 'csv'
def index
@users = User.all
respond_to do |format|
format.html
format.csv do
filename = ['Users', Date.today].join(' ')
send_data User.to_csv(@users), filename:, content_type: 'text/csv'
end
end
end
Add to_csv
method to the model
# app/models/user.rb
class User < ApplicationRecord
def self.to_csv(collection)
CSV.generate(col_sep: ';') do |csv|
csv << column_names
collection.find_each do |record|
csv << record.attributes.values
end
end
end
end
I think this is a great approach to export all records and their attributes without messing with templates.
4. Concern: (most generic approach) #
To make the #3 approach more scalable, we can extract the to_csv
into a model convern that can be shared across different models:
module GenerateCsv
extend ActiveSupport::Concern
require 'csv'
class_methods do
def to_csv(collection)
CSV.generate(col_sep: ';') do |csv|
# csv << attribute_names
csv << column_names
collection.find_each do |record|
csv << record.attributes.values
end
end
end
end
end
# app/models/user.rb
class User < ApplicationRecord
include GenerateCsv
end
That’s it!
Did you like this article? Did it save you some time?