Skip to main content

How do I Export a CSV file where it includes all the nested attributes?

Rails • Asked by Steve

Greetings:

I would like to export a CSV file where it includes all the nested attributes. I have an invoice model and a product model. Each invoice has many products. I am able to export to CSV following the GoRails video, but how do I include the data from the products page?

I'm fairly new to Rails so any help would be appreciated.

Many thanks,

Steve

Here's my code

class Invoice < ApplicationRecord
        has_many :products, dependent: :destroy

        def self.to_csv
            attributes = %w{Date Vendor_name Invoice_number}
            CSV.generate(headers: true) do |csv|
                csv << product.attributes.values

                all.each do |product|
                    csv << product.attributes.values_at(*attributes)
                end
            end
        end

end
<%= link_to "Export to CSV", invoices_path(@invoices, format: :csv), class: 'btn btn-info' %>
  def index
    @invoices = Invoice.all

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

What could be a solution is that you add associated records from the Invoice model. So within your attributes you call that method, eg. %w{ Date Vendor_name product_name}

    def product_name
        self.product.name
    end

Hi Jack,

Thank you for your response. Your idea is a good one, but I'm having problems calling the method. I'm assuming I put the method definition in the invoice controller, right? It doesn't seem to populate the product data.

Steve


No, the method should be in the model. That's where the data "lives".
The controller, just… erm… controls the data.


If the attribute names don't overlap, you could just use a Hash#merge on the attributes before you append them to the CSV.

For example, just change:

all.each do |product|
      csv << product.attributes.values_at(*attributes)
end

To:

all.each do |product|
      csv << product.attributes.merge(product.vendor.attributes).values_at(*attributes)
end

Where product.vendor.attributes would be the attributes for your nested model.

Since you're new to Rails, you may ask why this works.

The product object is an ActiveRecord model object but when you call .attributes on it, Rails will return the values as a Hash. This allows you to use Ruby and Rails Hash methods on it.

You can confirm the object class by opening your Rails console and running .class on the objects.

You can merge as many hashes as you need to for this. Just append .merge to it right before the .values_at method. If you start doing that, I'd recommend cleaning your code up by moving the merges to a new line and storing them in a variable. Then, you can call .values_at on that variable.


Hi John,

Many thanks. I'm somehow still getting an error that says "undefined method `title' for "

  def self.to_csv

    CSV.generate(headers: true) do |csv|
      csv << attributes = %w{date vendor_name invoice_number title  }

      all.each do |product|
            csv << product.attributes.merge(product.title.attributes).values_at(*attributes)
      end
    end
 end

I really appreciate your detailed explanation on how ActiveRecord works. It's very clear.

Steve


You're most welcome! :)

Would you be able to post the stack trace that you're receiving with that error?

An undefined method would usually mean a couple of things. Here's what's common:

  1. You have a typo in the association name that you're calling.
  2. That association doesn't yet exist. For example, one of your products doesn't have a title yet. If that's that case, you'd usually receive an "Undefined method 'title' for NilClass" <-- I'm betting this is that case here.

You may be able to find this error by adding a couple of logger lines right below the all.each do |product| line.

For example:

def self.to_csv

    CSV.generate(headers: true) do |csv|
      csv << attributes = %w{date vendor_name invoice_number title  }

      all.each do |product|
            Rails.logger.debug "Last Product: #{product.id}"
            Rails.logger.debug "Last Product Title: #{product.title}"
            csv << product.attributes.merge(product.title.attributes).values_at(*attributes)
      end
    end
 end

This will iterate over each product and allow you to see the last product in the iteration that caused the error.

By the way, what is the title object association for your product? What attributes? What type of association is it? etc.


Hi John,

When I put the code you gave me. The error I get is like this (assuming this is the stack trace):

NoMethodError in InvoicesController#index

  all.each do |product|
        Rails.logger.debug "Last Product: #{product.id}"
        ***  Rails.logger.debug "Last Product: #{product.title}"***
        csv << product.attributes.merge(product.title.attributes).values_at(*attributes)
  end
end

The attributes for products are sku, title, price, description, and quantity. In my product.rb model page, I got belongs_to :invoice. As follows:

class Product < ApplicationRecord
  belongs_to :invoice

end

invoice.rb

class Invoice < ApplicationRecord
  has_many :products, dependent: :destroy

  def self.to_csv
    CSV.generate(headers: true) do |csv|
      csv << attributes = %w{date vendor_name invoice_number title  }

      all.each do |product|
            Rails.logger.debug "Last Product: #{product.id}"
              Rails.logger.debug "Last Product: #{product.title}"
            csv << product.attributes.merge(product.title.attributes).values_at(*attributes)
      end
    end
  end

invoices_controller.rb

  def index
    @invoices = Invoice.order(created_at: :desc)

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

I think you're right. It's probably the case I'm not associating it. I'm just not sure what's wrong though. I've tried to fiddle with the controller, but no luck. Thanks again.

Steve


A stack trace is the entire error displayed in your application log. Here's an example of the stack trace that rails provides in the browser: https://i.stack.imgur.com/yrraB.png

I see what the issue is here. title is not an ActiveRecord Model object. It's an ActiveRecord model attribute for Product. With that said you won't be able to call #attibutes on it like: product.title.attributes. You should already be outputing the title into your CSV file with your code if you remove the merge.

Since, Invoice is the only association you have on your Product model, that's the only other object you can run #attributes on. For instance: product.invoice.attributes.


I'm not sure if you can see this, but this is my stack trace.

https://www.dropbox.com/s/s74aji1vg3ugjko/error.jpg?dl=0


Interesting. Would you please check your rails server logs to see how many "Last Product" lines processed?

In another terminal window, start the rails console via rails console and run Product. Please paste in what that says here.


When I just run Product, it says:
"Product (call 'Product.connection' to establish a connection)"

When I hit Product.last, it does output an entry.

=> #


Hmm I don't think the entry example you mentioned pasted in correctly. Would you be able to try again?


oh sorry, not sure why it did that.

Product id: 3, sku: "1234", title: "20inch Monitor", description: "used", price: "60", quantity: 2, invoice_id: 3, created_at: "2017-08-22 12:37:21", updated_at: "2017-08-22 12:37:21"


Hey Steve, did you add the merge for the title because you weren't seeing the title in your CSV file? What results are you hoping for and what are you receiving?


I can't seem to get any of my nested attributes like "title", "description", or "price." I'm hoping to export the file and then see the nested data that I inputed in the CSV file. To use the example above, I should see "20inch Monitor" for the title. I'm not receiving anything. Basically the CSV file is blank. I see the headings for title and all, but it's not populating the entries.


Hey Steve,

Just a real quick suggestion - use the byebug gem to figure out what's going on at that moment, see below where I added byebug

class Invoice < ApplicationRecord
  has_many :products, dependent: :destroy

  def self.to_csv
    CSV.generate(headers: true) do |csv|
      csv << attributes = %w{date vendor_name invoice_number title  }

      all.each do |product|
          byebug
          csv << product.attributes.merge(product.title.attributes).values_at(*attributes)
      end
    end
  end

Now in your console when you run the method, it will halt execution at that point and give you a console where you can start seeing what you're working with. So for example, if you type product in the console, you'll see the current product object for that iteration. From here you can start figuring out exactly what you need to do to get access to your associated objects.

If it only occurs on a certain record, you can do stuff like byebug if product.id == 24 so you don't have to manually continue over all the "good" objects.

Outside of this, could you provide a simplified repo so we can see what you're seeing and tinker around a bit?


Hi Steve, I see what the problem is. You CSV iterator is built for a has_one or belongs_to relationship.

In a has_one or belongs_to relationship, Active record will pass the object in directly. Which allows you to call something like:

Invoice.first.product # returns object

However, since you have a has_many relationship in your Invoice model, you need to treat the returned value as an array.

For example:

Invoice.first.products # returns an array of objects

You need to change how your iteration works.

First, you're iterating through invoices, not products.

Change all.each do |product| to all.each do |invoice|.

Then, you need to add another iterator because an invoice has_many products. That means, you need to go through each one of those products.

invoice.products.each do |product|

Next, you can place those products into the CSV file.

For example:

class Invoice < ApplicationRecord
  has_many :products, dependent: :destroy

  def self.to_csv
    CSV.generate(headers: true) do |csv|
      csv << attributes = %w{date vendor_name invoice_number title  }

      all.each do |invoice|
               invoice.products.each do |product|
                  csv << invoice.attributes.merge(product.attributes).values_at(*attributes)
               end
      end
    end
  end

Give that a shot and let me know if it works.


You're a genius! Thank you so much.


Login or Create An Account to join the conversation.

Subscribe to the newsletter

Join 24,647+ developers who get early access to new screencasts, articles, guides, updates, and more.

    By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

    More of a social being? We're also on Twitter and YouTube.