概要(はじめに)
- Railsでcsvインポートを実装したいと思って調べていた
- シンプルな登録処理のみでエラー処理がないものが多かった
- ユースケースを考えながら処理をクラスメソッドにまとめてコントローラで分岐させてみた
- コードは汚い
学習記録なので気分が悪くなったら読むのをお止めください。
環境
- Ruby 2.6.3
- Rails 5.2.4.4
よく見るインポート処理
require 'csv'
def self.import(file)
CSV.foreach(file.path, headers: true) do |row|
hoge = new
hoge.attributes = row.to_hash.slice(*csv_attributes)
hoge.save!
end
def import
Hoge.import(params[:file])
redirect_to hoge_path
end
参考: 現場で使える Ruby on Rails 5速習実践ガイド
思ったこと
上記参考書にもありますが、エラー処理がない、ファイル不備のチェックがない(ビジネスエラー時にどこがエラーを起こしているかがわからない、何件処理したのかがわからない)ことがとりあえずの問題かなと思いました。
実装を変えてみた
実装を変えたのは下記の通り
* 登録フォームが空の状態で登録した時のエラー処理
* クラスメソッドを2つに分けた(エラーチェック、登録処理)
* 新規登録、更新が何件されたかを表示する
* エラーが起きた時に具体的にどの部分が不正かを表示する
モデル
今回は商品モデルのインポートを実装しました(ユーザーとカテゴリが紐付く)
belongs_to :user
belongs_to :category
実装
実装したものがこちら
def self.csv_format_check(file)
errors = []
CSV.foreach(file.path, headers: true).with_index(1) do |row, index|
user = User.find_by(name: row["user_name"])
category = Category.find_by(name: row["category_name"])
errors << "#{index}行目 #{user_name}が不正です" if user.blank? # 出品者名が不正
errors << "#{index}行目 #{category_name}が不正です" if category.blank? # カテゴリ名が不正
if row["ID"].present?
product = find_by(id: row["ID"])
errors << "#{index}行目 IDが不正です" if product.blank? # IDが不正
else
u_id = user.id if user.present?
c_id = category.id if category.present?
product = new(title: row["title"], price: row["price"], user_id: u_id, category_id: c_id)
errors << "#{index}行目 新規作成できませんでした" if product.invalid? # 新規作成データが不正
end
end
errors
end
上記でエラー処理をまとめて、CSVファイル取り込み時にerrorsを吐き出すクラスメソッドにしました。with_indexで取り込みファイルの行数を取得します。
Ruby 2.7.0 リファレンスマニュアル(with_index)
def self.import_save(file)
new_count = 0
update_count = 0
nochange_count = 0
CSV.foreach(file.path, headers: true) do |row|
user = User.find_by(name: row["出品者名"])
category = Category.find_by(name: row["カテゴリ名"])
if row["ID"].present?
product = find(row["ID"])
product.assign_attributes(id: row["ID"], title: row["商品名"], price: row["値段"], user_id: user.id, category_id: category.id)
if product.changed?
product.save!
update_count += 1
else
nochange_count += 1
end
else
product = new(id: row["ID"], title: row["商品名"], price: row["値段"], user_id: user.id, category_id: category.id)
product.save!
new_count += 1
end
end
"新規作成:#{new_count}件、更新:#{update_count}件、無変更:#{nochange_count}件"
end
上記が登録処理のクラスメソッドで、新規作成と更新処理をします。最終的に何件処理をしたかを表示します。事前にエラー処理しているのでここではエラーがないことが前提になります(全てのエラーチェックができているかは不明)
def import
if params[:file].present?
if Product.csv_format_check(params[:file]).present?
redirect_to hogehoge_path, alert: "エラーが発生したため処理を中断しました。#{Product.csv_format_check(params[:file])}"
else
message = Product.import_save(params[:file])
redirect_to hogehoge_path, notice: "インポート処理が完了しました。#{message}
end
else
redirect_to hogehoge_path, alert: "インポート処理が失敗しました。ファイルを選択してください。"
end
end
コントローラで処理を分岐させています。ファイルの空チェック、エラー処理、登録処理の順に行います。
感想
綺麗にメタ的には書けてないのと、エラー処理が十分か不明なのでなんとも言えないですがサンプルコードをアレンジして気になった部分を実装してみました。どのモデルでも使えるようにモジュール化したり、エラー処理に加えてフォーマット(文字コード)のチェックなども入れるといいかなと思いました。
[追記(12/15)]csvヘッダに不正がある場合も対応したい。
[追記(12/19)]無変更の場合を追加、コントローラでクラスメソッドを2回呼び出してしまってたので修正
以上。最後まで読んでいただきありがとうございました。