Import CSV with validations - Rails
I. Chuẩn bị Giả sử mình cần import một số data vào database, thì CSV là format mình thấy đơn giản nhất, ở bài viết này chúng ta sử dụng gem Roo để hỗ trợ đọc file. Mặc định chúng ta có sẵn 2 model là User và Product và muốn import data vào cả 2 model này với 2 file CSV riêng. Đầu tiên, trong file ...
I. Chuẩn bị
Giả sử mình cần import một số data vào database, thì CSV là format mình thấy đơn giản nhất, ở bài viết này chúng ta sử dụng gem Roo để hỗ trợ đọc file. Mặc định chúng ta có sẵn 2 model là User và Product và muốn import data vào cả 2 model này với 2 file CSV riêng. Đầu tiên, trong file config/application.rb thêm require:
require "csv"
Trong Gemfile:
gem "roo"
II. Chi tiết
Controller
Chúng ta sẽ tách riêng 1 controller cho việc import để dễ xử lý: Trong controller/import_controller:
class ImportsController < ApplicationController def new @import = Import.new end def create @import = Import.new(params[:import]) if @import.save flash[:success] = "Imported successfully!" redirect_to new_import_path else render :new end end end
View
Ở view, chúng ta dùng form_for để submit file và lưu xuống database. Vì có thể import được vào 2 model là User và Product nên ngoài file cần truyền thêm 1 params là model_type:
<%= form_for new_import_path, method: :get do |f| %> <% if @import.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(@import.errors.count, "error") %> prohibited this import from completing:</h2> <ul> <% @import.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <div class="form-group"> <%= f.select :model_type, ["User", "Product"], {include_blank: "Model type"}%> </div> <div class="field"> <%= f.file_field :file %> </div> <div class="buttons"><%= f.submit "Import" %></div> <% end %>
Routes
Trong config/routes , thêm:
resources :imports, only: [:new, :create]
Model
Đây là phần quan trọng nhất. Model này ko được lưu ở database, nó chỉ đơn giản là một Ruby class, nhưng việc validate cũng giống như các model khác. Ở view chúng ta có truyền thêm params model_type nên ở đây cần thêm vào attr_accessor :
attr_accessor :file, :model_type
Ví dụ về file csv của model Product:
id,name,released_on,price,created_at,updated_at 4,Acoustic Guitar,2012-12-26,1025.0,2012-12-29 18:23:40 UTC,2012-12-29 18:23:40 UTC 5,Agricola,2012-10-31,45.99,2012-12-29 18:23:40 UTC,2012-12-29 18:23:40 UTC
Dòng đầu tiên là header, từ dòng thứ 2 trở đi là các data sẽ lưu vào db. Ở đoạn code bên dưới, gem Roo đã hỗ trợ đọc từ dòng thứ 2 đến dòng cuối cùng của file CSV, dựa vào params model_type để lấy đúng model. Trong trường hợp id trong file csv đã có trong db thì sẽ thực hiện update, nếu không sẽ thực hiện tạo mới:
def load_imported_master header, spreadsheet (2..spreadsheet.last_row).each do |i| row = Hash[[header, spreadsheet.row(i)].transpose] object = model_type.constantize.find_by(id: row["id"]) || model_type.constantize.new object.attributes = row.to_hash object end end
Hàm save như bên dưới, chỉ cho phép import CSV type. Chúng ta cũng kiểm tra luôn cho trường hợp file csv lấy vào ko đúng so với các thuộc tính trong model được chọn:
def save #Chỉ cho phép import file dạng CSV unless File.extname(file.original_filename) == ".csv" errors.add :base, I18n.t("import.unknown_file_type") return end spreadsheet = Roo::Spreadsheet.open file.path header = spreadsheet.row 1 header_model = model_type.constantize.column_names # Kiểm tra nếu file csv lấy vào ko khớp với model được chọn unless (header - header_model).empty? errors.add :base, "File CSV does not match. Pls try again!" return end load_imported_master header, spreadsheet if object.valid? object.save! else object.errors.messages.each do |_key, message| errors.add :base, "#{message}" end end end
Chúng ta đã có code hiển thị lỗi ở view, vì vậy việc cần làm tiếp theo là validate cho từng model User và Product, ở đây mình chỉ demo đơn giản nên validate not nil cho vài trường, các bạn có thể bổ sung thêm. Trong file models/user:
validates :name, :email, presence: true
tương tự cho các model khác.
Cuối cùng ta có thể ráp code lại như sau: models/import.rb:
class Import < ApplicationRecord attr_accessor :file, :model_type def initialize attributes = {} attributes.each{|name, value| send("#{name}=", value)} end def save #Chỉ cho phép import file dạng CSV unless File.extname(file.original_filename) == ".csv" errors.add :base, I18n.t("import.unknown_file_type") return end spreadsheet = Roo::Spreadsheet.open file.path header = spreadsheet.row 1 header_model = model_type.constantize.column_names # Kiểm tra nếu file csv lấy vào ko khớp với model được chọn unless (header - header_model).empty? errors.add :base, "File CSV does not match. Pls try again!" return end load_imported_master header, spreadsheet end def load_imported_master header, spreadsheet ActiveRecord::Base.transaction do (2..spreadsheet.last_row).each do |i| binding.pry row = Hash[[header, spreadsheet.row(i)].transpose] object = master_type.constantize.find_by(id: row["id"]) || master_type.constantize.new object.attributes = row.to_hash if object.valid? object.save! else object.errors.messages.each do |_key, message| errors.add :base, "#{message}" end end raise ActiveRecord::Rollback end end end
III. Kết luận
Như vậy chúng ta vừa tìm hiểu cách import csv với các validations, các bạn có thể thêm bớt hoặc chỉnh sửa code để hợp với mục đích của mình hơn. Hẹn gặp lại trong các loạt bài sau.
Link tham khảo
http://railscasts.com/episodes/396-importing-csv-and-excel