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 ...
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