LoginSignup
1
2

More than 3 years have passed since last update.

Railsでインポート処理を実装

Last updated at Posted at 2020-12-13

概要(はじめに)

  • Railsでcsvインポートを実装したいと思って調べていた
  • シンプルな登録処理のみでエラー処理がないものが多かった
  • ユースケースを考えながら処理をクラスメソッドにまとめてコントローラで分岐させてみた
  • コードは汚い

学習記録なので気分が悪くなったら読むのをお止めください。

環境

  • Ruby 2.6.3
  • Rails 5.2.4.4

よく見るインポート処理

config/application.rb
require 'csv'
hoge.rb
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
hoges.controller.rb
def import
  Hoge.import(params[:file])
  redirect_to hoge_path
end

参考: 現場で使える Ruby on Rails 5速習実践ガイド

思ったこと

上記参考書にもありますが、エラー処理がない、ファイル不備のチェックがない(ビジネスエラー時にどこがエラーを起こしているかがわからない、何件処理したのかがわからない)ことがとりあえずの問題かなと思いました。

実装を変えてみた

実装を変えたのは下記の通り
* 登録フォームが空の状態で登録した時のエラー処理
* クラスメソッドを2つに分けた(エラーチェック、登録処理)
* 新規登録、更新が何件されたかを表示する
* エラーが起きた時に具体的にどの部分が不正かを表示する

モデル

今回は商品モデルのインポートを実装しました(ユーザーとカテゴリが紐付く)

product.rb
belongs_to :user
belongs_to :category

実装

実装したものがこちら

product.rb
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)

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

上記が登録処理のクラスメソッドで、新規作成と更新処理をします。最終的に何件処理をしたかを表示します。事前にエラー処理しているのでここではエラーがないことが前提になります(全てのエラーチェックができているかは不明)

product_controller.rb
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回呼び出してしまってたので修正

以上。最後まで読んでいただきありがとうございました。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2