Export Csv Trong Rails(p2)

Trong bài trước mình có chia vẻ về cách Export CSV thông qua một demo đơn giản(export thông tin của một model). Hôm nay mình sẽ tăng độ khó lên 1 chút đó là export thông tin của nhiều bảng một lúc.

Link phần 1:

Export Csv Trong Rails(p1)

Source code:

Export csv

Mình sẽ lấy ví dụ đơn giản là user sẽ biết nhiều ngôn ngữ và có nhiều chứng chỉ. Oke mình sẽ thêm 2 model quan hệ với user mình tạo hôm trước:

  • Model Language
  • Model Certification

Tạo model quan hệ với user

Tạo model

rails g model language name:string
rails g model certification name:string

Thêm quan hệ vào file migrate

class CreateLanguages < ActiveRecord::Migration[5.2]
  def change
    create_table :languages do |t|
      t.string :name
      t.references :user

      t.timestamps
    end
  end
end
class CreateCertifications < ActiveRecord::Migration[5.2]
  def change
    create_table :certifications do |t|
      t.string :name
      t.references :user

      t.timestamps
    end
  end
end

Migrate

rails db:migrate

Thêm quan hệ cho model

class User < ApplicationRecord
  has_many :languages, dependent: :destroy
  has_many :certifications, dependent: :destroy
  
  # Mình sẽ xóa dòng này đi vào khai báo ở chỗ khác
  #CSV_ATTRIBUTES = %w(name address phone).freeze
end
class Certification < ApplicationRecord
  belongs_to :user
end
class Language < ApplicationRecord
  belongs_to :user
end

Ở bài trước mình có khai báo các attributes cần export trong model User, để tránh model phình to và dễ bảo trì mình sẽ tạo 1 model ảo để khai báo các constant cho phần export csv này:

class ExportCsv::RowFormat
  RELATIONSHIP_EXPORT_CSV = %w().freeze
  
  ARR_DECORATE_RELATION = %w().freeze

  ATTRIBUTE_EXPORT_CSV = %w().freeze

  ATTRIBUTES_LANGUAGE = %i().freeze
  ATTRIBUTES_CERTIFICATION = %i().freeze
end

Cài gem Draper

Tại sao cần dùng gem Draper? Vì khi chúng ta export không phải lúc nào chúng ta cũng lấy nguyên dữ liệu từ database ra, đôi khi chúng ta phải biến tấu dữ liệu sao cho phù hợp với yêu cầu đặt ra. Các bạn muốn tìm hiểu về gem này thì xem bài biết này nhé Draper in Rails

Thêm đoạn này vào gemfile và tiến hành bundle

gem "draper"

Tạo decorator cho user

rails g decorator User

Khai báo constant cần thiết và thêm các hàm cần thiết trong decorator

Khai báo constant trong model ảo:

  • RELATIONSHIP_EXPORT_CSV: các quan hệ cần export
  • ARR_DECORATE_RELATION: tên các hàm decorate lấy dữ liệu của các quan hệ
  • ATTRIBUTE_EXPORT_CSV: attribute cần export
  • ATTRIBUTES_LANGUAGE: attribute cần export của Language
  • ATTRIBUTES_CERTIFICATION: attribute cần export của Certification
class ExportCsv::RowFormat
  RELATIONSHIP_EXPORT_CSV = %w(languages certifications).freeze

  ARR_DECORATE_RELATION = %w(arr_languages arr_certifications).freeze

  ATTRIBUTE_EXPORT_CSV = %w(name address phone arr_languages arr_certifications).freeze

  ATTRIBUTES_LANGUAGE = %i(name).freeze
  ATTRIBUTES_CERTIFICATION = %i(name).freeze
end

Khai báo các hàm lấy dữ liệu trong decorator:

class UserDecorator < Drape::Decorator
  delegate_all

  def arr_languages
    arr_relationship_export languages, ExportCsv::RowFormat::ATTRIBUTES_LANGUAGE, 5
  end

  def arr_certifications
    arr_relationship_export certifications, ExportCsv::RowFormat::ATTRIBUTES_LANGUAGE, 5
  end

  private
  def arr_relationship_export objects, attributes, max_column_export
    arr_relationship = objects.limit(5).map do |object|
      attributes.map do |attr|
        object.public_send(attr)
      end
    end.flatten
    number = max_column_export - arr_relationship.count
    arr_relationship.concat([""] * number)
  end
end

Ở đây mình sẽ fix cứng lấy ra thành 5 cột dạng ["value 1", "value 2", "", "", ""], nếu không có giá trị thì để phần tử mảng rỗng

Thêm settings

Các bạn nhớ cài thêm gem config nhé

languages: languages
certifications: certifications
decorate_languages:
  - language_name
decorate_certifications:
  - certification_name

Thêm I18n

en:
  home:
    index:
      link_export: Export Users
      title: Export CSV
  header_csv:
    name: Full Name
    address: Address
    phone: Phone Number
    language_name: "Language_%{number}"
    certification_name: "Certification_%{number}"

Tạo service khởi tạo header cho file csv

Mình sẽ tạo header theo dạng ["name", "phone", "address", "language_1", "language_2", ...] Đoạn code dưới đây sẽ lặp các attributes rồi push các giá trị I18n tương ứng vào mảng và tạo thành header cho file csv

class HeaderCsv
  def initialize attributes, csv
    @attributes = attributes
    @csv = csv
  end

  def perform
    arr_header = attributes.inject([]) do |arr, attr|
      if ExportCsv::RowFormat::ARR_DECORATE_RELATION.include? attr
        5.times do |n|
          decorate_relationship(attr).each do |decorate|
            arr << (I18n.t("header_csv.#{decorate}", number: n.next))
          end
        end
      else
        arr << (I18n.t("header_csv.#{attr}"))
      end
      arr
    end
    csv << arr_header.map { |header| header }
  end

  private
  attr_reader :attributes, :csv

  def decorate_relationship attr
    relation = ExportCsv::RowFormat::RELATIONSHIP_EXPORT_CSV.detect{ |r| attr.include?(Settings.public_send r) }
    Settings.public_send("decorate_#{relation}")
  end
end

Tạo service lấy data

Service này cũng tương tự service tạo header, nó sẽ lấy value tương ứng push vào 1 mảng

class DataCsv
  def initialize attributes, candidate
    @attributes = attributes
    @candidate = candidate
  end

  def perform
    attributes.inject([]) do |arr, attr|
      if candidate.public_send(attr).is_a?(Array)
        candidate.public_send(attr).each { |value| arr << value }
      else
        arr << candidate.public_send(attr)
      end
      arr
    end
  end

  private
  attr_reader :attributes, :candidate
end

Sửa lại service export csv

Ở service này gọi đến 2 service trên để tạo content cho file csv

require "csv"

class ExportCsvService
  def initialize objects
    @attributes = attributes
    @objects = objects.decorate
  end

  def perform
    CSV.generate encoding: Encoding::SJIS do |csv|
      @csv = csv
      HeaderCsv.new(ExportCsv::RowFormat::ATTRIBUTE_EXPORT_CSV, csv).perform
      export_content
    end
  end

  private
  attr_reader :attributes, :objects, :csv

  def export_content
    objects.each do |object|
      push_line object
    end
  end

  def push_line object
    data_export = DataCsv.new(ExportCsv::RowFormat::ATTRIBUTE_EXPORT_CSV, object).perform
    csv << data_export
  end
end

Update lại controller

class ExportUsersController < ApplicationController
  def index
    csv = ExportCsvService.new User.all
    respond_to do |format|
      format.csv { send_data csv.perform,
        filename: "users.csv" }
    end
  end
end

Như vậy là chúng ta đã hoàn thành được chức năng export CSV, bài viết mình xin kết thúc ở đây, mong nhận được sự góp ý của các bạn. Cảm ơn các bạn đã quan tâm đến bài viết này!

source code: https://github.com/cuongtobi/export_csv