webエンジニアの日常

RubyやPython, JSなど、IT関連の記事を書いています

RailsでCSVインポート機能を実装する

こんにちは、さもです。

CSVをアップロードしてデータの一括投入機能はよくあると思いますが、最も基本的な実装をメモしておきます。

イメージはこんな感じです。

f:id:s-uotani-zetakansu:20180214152424p:plain

インポートにはactiverecord-importを使ってバルクインサートしています。

スポンサーリンク

Model

サンプルとして、次のようなEmailテーブルを作ってみましょう

  • マイグレーション
class CreateEmails < ActiveRecord::Migration
  def change
    create_table :emails do |t|
      t.string :email, null: false
      t.string :name
      t.timestamps null: false
    end
  end
end
  • モデル
class Email < ActiveRecord::Base
  validates :email, presence: true
end

View

次にファイルアップロード部分です。

  • emails/new.html.erb
<% if flash[:notice] %>
  <div class="alert alert-info" role="alert"><%= flash[:notice] %></div>
<% end %>

<div>
  <%= form_tag emails_path, method: :post, multipart: true do |f| %>
    <div class="search_item">
      <%= text_field_tag 'filename',"", id: "filename", disabled: true %>
      <%= file_field_tag 'emails_file', id: "file_input", style: "display: none;", onchange: "file_selected($(this));" %>
      <%= button_tag 'ファイル選択', class: %i(btn-primary csv_input_btn), type: 'button', onclick: "$('#file_input').click();" %>
    </div>
    <div>
      <button type="submit" class="btn btn-primary">CSVインポート</button>
    </div>
  <% end %>
</div>

<script type="text/javascript">
  function file_selected(file_field){
    var filename = $(file_field)[0].files[0].name;
    $("#filename").val(filename);
  }
</script>

ファイル選択のところで、text_field, file_field, buttonの三つがあるパターンはよくあると思います。

file_field自体がファイル選択ボタンを表示させるのですが、デザインがあまりいけていないです。

なので、かっこよくデザインされたボタンを新たに追加して、このボタンが押されたら、隠してあるファイル選択を実行します。

disabledになっているテキストフィールドは、選択したファイル名の表示用です。

ファイルを選択すると、file_fieldのonchangeが発火し、file_selected関数が呼ばれます。

file_selected関数は単に、選択されたファイルの名前をテキストフィールドに表示しています。

(注)忘れがちなのですが、ファイルを送る場合は、form_tag のなかでmultipart: trueを指定してください。

ビューはこんな感じです。

Gemfile

冒頭でも言いましたが、バルクインサートしたいので、Gemfileに次を追記して、bundleしておきます

# bulk insert
gem 'activerecord-import'

バルクインサートとは、複数のレコードを登録する際に、1レコードごとにinsertするのではなく、まとめてinsertする機能です。

SQLを生成してinsert、というのを繰り返さなくていいので、レコード数が多いときに特に登録処理が速くなります。

Controller

ではサーバでの処理を作っていきましょう

class EmailsController < ApplicationController

  def new
  end

  def create
    registered_count = import_emails
    redirect_to emails_path, notice: "#{registered_count}件登録しました"
  end

  private

  def import_emails
      # 登録処理前のレコード数
      current_email_count = ::Email.count
      emails = []
      # windowsで作られたファイルに対応するので、encoding: "SJIS"を付けている
      CSV.foreach(params[:emails_file].path, headers: true, encoding: "SJIS") do |row|
        emails << ::Email.new({ name: row["name"], email: row["email"] })
      end
      # importメソッドでバルクインサートできる
      ::Email.import(emails)
      # 何レコード登録できたかを返す
      ::Email.count - current_email_count
  end
end

単純にcsvの各行からEmailのインスタンスを作り、emails へ挿入しています。

activerecord-importでは、インスタンスの配列をimportメソッドによってバルクインサートしてくれます。

登録に成功すると、import_emailsは登録したレコード数を返すので、その数を画面に表示します。

まとめ

簡単でしたがCSVアップロード&一括登録の実装でした。

そういえばこの前、あるエラーの対処法を探していてあるブログを見つけて無事解決できたのですが、そのブログが、このブログでした。

168日前の自分ありがとう!

この記事もまた自分や他のエンジニアの方々の役に立ってくれることを願います。

読者登録はこちらからお願いします。

Ruby on Rails 5アプリケーションプログラミング

Ruby on Rails 5アプリケーションプログラミング

Ruby on Rails 5 超入門

Ruby on Rails 5 超入門