In this post, I’m going to cover exporting to csv in Rails 5, but with the added complexity of exporting a Parent and Child relationship.

To get started, I’d like to highlight a couple posts that I used to accomplish a simple export for a single model. I found this extremely useful and highly recommend them.

So let’s assume I have a User parent model and Coupon child model as follows:


# Parent model: User
name:string
age:integer
notes:text

has_many :coupons, dependent: :destroy

# Child model: Coupon
title:string

belongs_to :user

My initial attempt followed the articles above and I was able to get an export for the Users model without much issue. The steps are as follows:

Within the users/index.html.erb, let’s add a button that a user will be able to click to export our users.


<%= link_to "Download CSV", users_path(format: "csv") %>

Now let’s go ahead and update our index action in our Users controller to handle the csv export in addition to html.


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

And finally, let’s head over to our User model and add wire up the to_csv method we are calling above:


require 'csv'
class User < ApplicationRecord
    def self.to_csv(options = {})
      CSV.generate(options) do |csv|
        csv << column_names
        all.each do |product|
          csv << product.attributes.values
        end
      end
    end
end

We need the require csv library declaration at the top.

Now the above will work really well, and when you click on the download link, it will download a csv file with the naming convention we specified. However, what if we wanted to include any child relationships with that parent model as a part of the export? And to take it a step further, because of the has_many relationship, we have many coupons belonging to a single user.

In order to do so, we need to update our to_csv method to include the child attributes. Basically, we are iterating through the coupons and appending them to a string that we’ll include in the csv export. If we were simply to write:


csv << [user.name, user.age, user.notes, user.coupons]

We’d end up with #<Coupon::ActiveRecord_Associations_CollectionProxy> exported which is not what we want. I used a newline separator, but you can use whatever you want. I’ve also used loop for clarity, but you could also use map with the :& operator to condense to one line like this:


user.coupons.map(&:title).join("\n").

Here’s the updated to_csv method that includes the child attributes:


def self.to_csv(options = {})
  desired_columns = ["Name", "Age", "Notes", "Coupons"]
  CSV.generate(options) do |csv|
    # header columns
    csv << desired_columns
    # data columns
    all.each do |user|
      coupons = ""
      user.coupons.each do |coupon|
        coupons << coupon.title + "\n"
      end
      csv << [user.name, user.age, user.notes, coupons]
    end
  end
end

One thing that I did notice in the export was that if I had a column of type text whose value was blank the spreadsheet wasn’t recognizing that column in the row. So I made the following tweak:


def self.to_csv(options = {})
  desired_columns = ["Name", "Age", "Notes", "Coupons"]
  CSV.generate(options) do |csv|
    # header columns
    csv << desired_columns
    # data columns
    all.each do |user|
      coupons = ""
      user.coupons.each do |coupon|
        coupons << coupon.title + "\n"
      end
      csv << [user.name, user.age, user.notes.presence || " ", coupons]
    end
  end
end

Notice that .presence which detects a value, and if not, outputs a space to ensure that row/column isn’t blended and appears distinctly.

That should be it! You should have a proper output now, if you have any issues, please feel free to leave a comment and I’ll help out!