0

Import CSV file dùng cho nested attributes trong rails sử dụng CSV framework

Mình xin giới thiệu cách để import csv file dùng cho nested attributes sử dụng csv framework.

Mục đích của việc import là có thể thuận tiên hơn cho việc chuẩn bị data đầu vào trong các hệ thống hoặc nhập báo cáo thông kê tài chính hàng tháng chẳng hạn... Nó sẽ nhanh và thuận tiện hơn rất nhiều so với các bạn nhập thủ công bằng tay. Vì với số lượng bản ghi lớn, nhập bằng tay sẽ mất rất nhiều thời gian, chưa kể còn có thể có sai sót trong quá trình nhập liệu.

Mình sẽ lấy ví dụ 1 bài toán đặt ra: bạn có 2 model question(id,content) và answer(id,content,is_correct,question_id), 1 question có thể có nhiều answer (từ 2 tới 5), và bạn muốn import question và answer bằng file CSV. Mình xin giới thiệu một cách làm đơn giản như sau:

  • Đầu tiên trong file config/application.rb bạn thêm require "csv"
require "csv"
  • Tiếp theo bạn tạo thêm controller Csv để import và export file CSV. Sau đó khai báo trong routes:

csv_controller.rb

class CsvController < ApplicationController
   def create
     Question.import params[:file]
     redirect_to root_url, notice: "Question imported"
   end
 end

routes.rb

resources :csv, only: [:create]
  • Trong question.rb khai báo Answers là nested attributes:
has_many :answers, dependent: :destroy, inverse_of: :question
accepts_nested_attributes_for :answers, allow_destroy: true,
reject_if: proc{|attributes| attributes["content"].blank?}

Trong answer.rb khai báo thêm điều kiện:

belongs_to :question

Các bạn lưu ý là thêm inverse_of: :question nếu không khi bạn import sẽ báo lỗi là "question must exist" do khi đó question chưa được thêm vào database.

  • Trong file question.rb chúng ta thêm method import:
class << self
     def import file
       CSV.foreach(file.path, headers: true, col_sep: "|", header_converters: :symbol) do |row|
         row = row.to_hash
         answers_attributes = []
         row[:answers].split(";").each do |answer|
           answer_hash = Hash.new
           arr_answer = answer.split(",")
           answer_hash[:content] = arr_answer[0]
           answer_hash[:is_correct] = arr_answer[1]
           answers_attributes.push answer_hash
         end
         row[:answers_attributes] = answers_attributes
         row.delete :answers
         Question.create! row
       end
     end

Mình xin giải thích qua một chút về đoạn code trên:

  • headers: true bỏ qua dòng đầu tiên trong file csv (column_names), với mỗi dòng tiếp theo là tương ứng 1 record trong database
  • header_converters: :symbol convert header: The header String is downcased, spaces are replaced with underscores, non-word characters are dropped, and finally to_sym() is called. (viết tiếng anh này mọi người sẽ dễ hiểu hơn)
  • col_sep: kí hiệu ngăn cách giữa các column trong csv file, ở đây mình dùng "|"
  • mình tạo ra 1 hash là answers_attributes để lưu nested answers. file csv template:
content|answers
math|toan,true;ly,false
physical|toan,false;van,false;ly:true

ở đây các answer được ngăn cách nhau bằng đâu ";", và các attributes của answer được ngăn cách nhau bởi dấu "," nên mình xử lý như trên, cuối cùng mình xóa phần tử answers đi (vì thừa) và thực hiện create.

  • Cuối cùng là tạo button để import:
<%= form_tag csv_create_path, multipart: true do %>
  <%= file_field_tag :file %>
  <%= submit_tag "Import" %>
<% end %>

Trên đây là phần hướng dẫn của mình. Nó còn nhiều hạn chế ví dụ như chưa validate được trong lúc import,... Mong các bạn góp ý thêm. 😄


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí