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