search
LoginSignup
1

More than 1 year has passed since last update.

posted at

updated at

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

概要(はじめに)

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

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

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
What you can do with signing up
1