Rails 5.2は、Active Storageを標準でサポートしています。これとActive Jobを組み合わせると、CSVファイルのインポートを手軽に実装できます。
Motivation
CSVファイルをインポートする際、それが小さなファイルであれば、フロント側で処理を行うのがシンプルです。しかしそれが、大きなファイルや、複雑な処理を伴う場合、一旦はサーバー側にファイルを保存し、バックグラウンドでそれを処理する場合があります。
ファイルの保存に、Active Storageを使用すれば、保存先はAWS S3でも、ファイルシステムでも対応が可能です。Herokuではファイルシステムに保存されたファイルの永続化が保証されない(=サーバーに保存したファイルは、ある日突然消えることがある)ので、AWS S3などにファイルを保存することになります。
特に日本語圏のエンジニアを悩ませるのが、Microsoft ExcelがUTF-8ファイルを扱えず、SJISからのエンコード変換が必要になる点ですが、それにも対処を行います。
Approach
- (Web)アップロードされたファイルの保存 - アップロードされたファイルを受け取った際、サーバーはActive Storageを利用して、それをストレージに保存します。
-
(Active Job)ファイルをダウンロード -
作成するJobは、はじめにファイルをActive Storageからダウンロードし、一時的に/tmp
以下にファイルを保存します。 -
(Active Job)ファイルのエンコード形式を変換 -
SJIS
で保存されたファイルを、UTF-8
に変換します。 - (Active Job)DBに保存後、一時ファイルを削除
Procedure
1. 設定と、必要なGemのインストール
AWSの設定項目などは、環境変数に書き込みます。ローカルの開発環境では、Dotenvを利用します。
# Dotenv - 頭の方に書く
gem 'dotenv-rails', groups: [:development, :test]
amazon:
service: S3
access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
region: <%= ENV['AWS_S3_REGION'] %>
bucket: <%= ENV['AWS_S3_BUCKET'] %>
AWS_ACCESS_KEY_ID='' #AWSアクセスキー
AWS_SECRET_ACCESS_KEY='' #AWSシークレットキー
AWS_S3_REGION='us-east-1' #AWS S3リージョン
AWS_S3_BUCKET='' #AWS S3バケット名
バックエンド処理は、Active Job経由で、Sidekiqを使用するので、インストールを行っておきます。
# Backend job
gem 'sidekiq'
config.active_job.queue_adapter = :sidekiq
Gemをインストールします。
$ bundle install
サンプルでCSVファイル内のデータを挿入する、User
モデルを作っておきます。
$ rails g model User name_first:string name_last:string
2. ファイルがアップロードされるごとに、レコードを保存するテーブルを作成
ファイルのアップロードごとに、DBに対応するレコードを作ります。
$ rails g model CsvUpload imported_at:datetime
$ rails db:migrate
class CsvUpload < ApplicationRecord
has_one_attached :csv_file
end
3. アップロード処理
$ rails g controller CsvUploads
def new
@csv_upload = CsvUpload.new
end
def create
csv_upload = CsvUpload.new(csv_upload_params)
if csv_upload.save
ImportCsvFileJob.perform_later(csv_upload.id) #後述
redirect_to new_csv_upload_path, notice: 'アップロードに成功しました'
else
redirect_to new_csv_upload_path, alert: 'アップロードに失敗しました'
end
end
private
def csv_upload_params
params.require(:csv_upload).permit(:csv_file)
end
<%= form_for @csv_upload do |f| %>
<%= f.file_field :csv_file %>
<% end %>
4. インポートするJobの作成
まず、GeneratorでJob
を作ります。
$ rails g job ImportCsvFile
ここが一番の山場です。Active Storageに保存したファイルを、ローカルに読み込み、そのデータを挿入していきます。挿入完了後、CsvUpload
のimported_at
を更新してインポートが成功したことを保存します。ファイルの途中まではインポートが成功し、一部のユーザのみインポートが完了している、などの状態が発生すると非常に面倒なので、一連の動作はActiveRecord::Base.transaction
で囲い、トランザクションをかけています。
Excelのファイルフォーマットは、正確にはSJIS
ではなくCP932
なので、CP932
からUTF-8
にエンコーディングの変更を行っています。
require 'csv'
class ImportCsvFileJob < ApplicationJob
queue_as :default
def perform(csv_upload_id)
csv_upload = CsvUpload.find(csv_upload_id)
tmp_file_path = Rails.root.join('tmp', 'csv_file.csv')
# 一時ファイル書き込み
file_content = csv_upload.csv_file.download
File.open(tmp_file_path, 'wb') do |file|
file.write(file_content)
end
# ユーザテーブルにデータ挿入
ActiveRecord::Base.transaction do
CSV.foreach(tmp_file_path, encoding: 'CP932:UTF-8') do |line|
User.create!(name_first: line[0], name_last: line[1])
csv_upload.update!(imported_at: Time.zone.now)
end
end
# 例外発生時を含め、一時ファイルは必ず削除する
ensure
File.delete(tmp_file_path) if File.exist?(tmp_file_path)
end
end
ファイルをローカルにダウンロードする部分は、Active Storageの標準機能として、今後実装されるようです。Active Strageで検索を行うと、Edgeのドキュメントがヒットし、そこにはDownloading Filesという項目がありますが、Rails 5.2では未実装のようでした。
Possible improvements
サンプルでは触れませんでしたが、このような点は改善できます。
ファイルアップロード時のバリデーション
フォーマットをしっかりとバリデーションするには多くの時間がかかりますが、少なくともCSV形式であることなどは、バリデーションを行うことができます。
ファイルエンコード形式をUTF-8にも対応
import_csv_file_job.rb
内でSJIS
のみをエンコード形式として想定していますが、これはCharlock HolmesなどのGemを使用して、自動的に判別することができます。
インポート成功、失敗時に通知
本サンプルのフローでは、ユーザがCSVファイルをアップロードしたタイミングでは、インポートの成功、失敗はわかりません。そのため成功、失敗時にメールを送るような処理が考えられるでしょう。
成功の通知は、ユーザテーブルにデータ挿入するトランザクションが完了した後に行うのが良いでしょう。
失敗の通知は、例えば、failure_count
をCsvUpload
に作り、失敗した回数が一定回数に達したときに、通知を行うようにします。ImportCsvFileJob#perform
内に、rescue
clauseを作り、その中でfailure_count
をインクリメントするような処理が考えられます。Sidekiqは、リトライ機能を標準で搭載しています。
Conclusion
本記事では、実際にサンプルを作りながら、Active Storage経由でCSVファイルをアップロード・ダウンロードし、エンコード形式を変えながら、バックエンド処理でデータのインポートを行う方法を解説しました。このサンプルは、できるだけ"The Rails Way"に沿う方法で実装を行いましたが、ここを起点として様々な拡張が可能です。
References
- Active Storage Overview - https://edgeguides.rubyonrails.org/active_storage_overview.html
- Active Storage on Heroku - https://devcenter.heroku.com/articles/active-storage-on-heroku
- 本当は怖くないCP932 - https://qiita.com/kasei-san/items/cfb993786153231e5413