【Rails】ネストされた、子要素にCSVインポート機能を実装してみる。

今回は、CSVファイルのインポート(一括入力)処理を作っていきます。

スクールのカリキュラムに載っていない内容なので

書く前からそわそわしております・・・。

間違っていたら、優しく教えていただけるとありがたいです。

おぢさんのメンタルはやわらか戦車よりやわらかいので・・・。

と言う事で、最終的な完成形の流れはこんな感じです。

  • 1.最初は、親のcalendaer(入荷日)を決めます。

gyazo.com

  • 2.親のcalendaer(入荷日)に関連する入荷物をCSVファイルから一括入力します。

gyazo.com

すると、入荷物を一括で見る事ができます。

gyazo.com

目次

 

1.前提条件

1-1.モデル

親(calender)

class Calendar < ApplicationRecord
has_many :stocks, dependent: :destroy

 
end

わかりやすくする為、バリデーションは省いています。

マイグレーションは割愛します。

ポイントは

  • アソシエーションを記述して、stockモデルとの親子関係を定義
  • 「dependent: :destroy」を記述して、親を削除すると関連する子のデータも消す

子(stock)

class Stock < ApplicationRecord
belongs_to :calendar

 
 
 
end

こちらのマイグレーションは今はここまで。

後で修正をかけるのですが、現時点では割愛します。

ポイントは

  • アソシエーションを記述して、stockモデルとの親子関係を定義
1-2.ルーティング
Rails.application.routes.draw do
resources :calendars do
resources :stocks
end
end

ネストは、ある記述の中に別の記述をして、親子関係を示す方法です。

入れ子構造」とも呼ばれますね。

と言う事で、calenderコントローラーのルーティングの中に、stockコントローラーのルーティングを記述しています。

1-3.コントローラー

こちらは、実装の主役であるstockコントローラーだけです。

class StocksController < ApplicationController
def index
@calendar = Calendar.find(params[:calendar_id])
@stocks = @calendar.stocks
end

 
end

今のところ、定義するのはindexだけですね。

親(calender)のデータと

付随する子(stock)のデータを全て出力します。

1-4.ビュー

こちらも、今回の主役stock/index.html.erbだけ。

〜省略〜
 
<table>
<thead>
<tr>
<th>陳列</th>
<th>出版社名</th>
<th>雑誌名</th>
<th>冊数</th>
<th>本体価格</th>
<th>発行形態</th>
<th>買切雑誌</th>
</tr>
</thead>
<tbody>
<% @stocks.each do |stock| %>
 
<tr>
<td><%= stock.display %></td>
<td><%= stock.publisher %></td>
<td><%= stock.magazine_name %></td>
<td><%= stock.num %></td>
<td><%= stock.price.to_s(:delimited) %></td>
<td><%= stock.i_form %></td>
<td><%= stock.purchased %></td>
 
</tr>
<% end %>
</tbody>
</table>

大事なところだけ抜き出しています。

登録したデータをエクセルの様な感じで表示してくれます。

csvのファイルデータは他にもっとあるのですが、

画面に表示するのは7項目だけって事ですね。

1-5.テーブル(表)タグの説明

<table>はテーブル(表)を作成するタグですね。

テーブル(表)の基本的な構造としては

<table>~</table>内に<tr>〜</tr>で表の横一行を定義して

さらにその中に<th>〜</th>や<td>〜</td>でセルを定義したりします。

テーブル(表)の各セルには

見出しを定義するヘッダセル(<th>〜</th>)と、

データを定義するデータセル(<td>〜</td>)があります。

ヘッダセル内のテキストは、一般的な画面では太字で中央に表示されるみたいです。

後は、スタイルシートで装飾してあげます。

2.課題は山積み

今回の実装をするにあたり、解決しないといけない事が結構ありまして

先ずはそちらを書き出していきます。

  • CSVファイルを読み込む記述をする。
  • CSVファイルのヘッダー(カラム名)が日本語だった。
  • CSVファイルを読み込む際、親のidをどうやって入れよう。

大きくはこの3点です。

あ〜でもない、こ〜でもないと苦悩の日々が始まります。

3.CSVファイルを読み込む準備

先ずはCSVファイルなどを読み込む事ができるGem「Roo」を読み込みます。

3-1.Gem「roo」

「Roo」は、スプレッドシートタイプの読み取りアクセスができるライブラリで

今回はCSVを読み取るために使わせていただきます。

他にも

などが処理できる様です。すごい便利!

ライブラリをGemfileに追加し、bundle installしちゃいましょう。

gem 'roo'
3-2.CSVファイルを確認

今回使うCSVファイルを用意します。


書店コード,地区名,書店名, 〜省略〜,ISBNコード,
123456,テスト,テスト,〜省略〜, ,
123456,テスト,テスト,〜省略〜, ,
123456,テスト,テスト,〜省略〜, ,

めっちゃ項目が多いので省略しまくりました。

ポイントとしては、ヘッダーが日本語表記だと言う事ですね。

3-3.マイグレーションを記述

CSVファイルの内容に合わせてマイグレーショを記述していきます。

class CreateStocks < ActiveRecord::Migration[6.0]
def change
create_table :stocks do |t|
t.string :store_code, comment: '書店コード'
t.string :district, comment: '地区名'
t.string :store_name, comment: '書店名'
〜〜省略〜〜
 
t.string :isbn, comment: 'ISBNコード'
t.references :calendar, null: false, foreign_key: true
t.timestamps
end
end
end

 

4.importアクションを設定(ルーティングの記述変更 )

config/routes.rbに下の様に記述を追加します。

Rails.application.routes.draw do
resources :calendars do
resources :stocks do
下記一行を追加
collection { post :import }
end
end
end

 これで、

gyazo.com

この様に、importアクションが追加されました。

5.ビューを実装(フォームを作成)

5-1.コントローラーを記述追加

今回は、「new」画面でフォームを作成したかったので

「newアクション」を先に記述追加します。

class StocksController < ApplicationController
def index
@calendar = Calendar.find(params[:calendar_id])
@stocks = @calendar.stocks
end

def new
@calendar = Calendar.find(params[:calendar_id])
end

 
end

ポイントは親のidを忘れずに持ってくるぐらいですかね。

5-2.ビュー作成。

そして、ビューですがこんな感じです。

<%= form_with url: import_calendar_stocks_path do |f| %>
<h3>CSVファイルを入力してください</h3>
<%= f.label :入荷物%>
<%= f.file_field :file, accept: '.csv' %>
<%= f.submit "登録"%>
 
<% end %>

ポイントは3つ。

  1. 「form_with」を使うといろいろ省略できて便利。
  2. 「method: :get」をつけない。
  3. 「accept: '.csv'」 を記述する。

順に説明しますと

補足説明1.form_with」を使うといろいろ省略できて便利。

csvインポートを実装したくて、Qiitaや各ブログを拝見していましたら

多くの方が

<% form_tag import_books_path, multipart: true do %>
<%= file_field_tag :file %>
<%= submit_tag "インポート"%>

と、言う様な記述をされていたのですよね。

この「multipart(マルチパート)」は

画像やCSVなどのファイルを読み込む際に必要になってくる記述なのですが

form_withを使う場合は省略可能で、

form_with内にfile_fieldが存在すれば自動的にmultipartが適用されちゃうそうです!

便利な世の中になったもんだ・・・。

これは、Rails 5からだそうです。

カリキュラムでは何も知らずに使っていましたがバージョンアップってすごい!

補足説明2.「method: :get」をつけない。

getリクエスト(form_withにmethod: :getを指定する)にしてしまうと

パラメータがnilになっちゃうらしくでデフォルトのpostのままにします。

補足説明3.「accept: '.csv'」 を記述する。

「file_field」に「 accept: '.csv'」 を書くことで、

csvファイルだけが選択される様になります。

(ファイル選択時、csvファイル以外は非活性状態になってます)

 6.CSVインポート作業を実装

も〜、ここで1週間ぐらい悩みました。

自分なりに出した結論は以下の様な感じです。先ずはコントローラーから

6-1.importアクションを記述

長くなりそうなので、インポートアクションのみ記述します。

def import
@calendar = Calendar.find(params[:calendar_id])
Stock.import(params[:file], params[:calendar_id])
redirect_to root_path, notice: "#{l @calendar.day, format: :long}の入荷物を登録しました。"
end 

ポイントは記述順に

  • 親のデータを取得しておく
  • importで送られるデータはcsvのファイルと親のidにしておく。
  • ちゃんと登録できたか、確認のためにフラッシュも付けておく。
6-2.モデルに記述追加

こちらも長くなりそうなので所々省略します。

class Stock < ApplicationRecord
belongs_to :calendar

def self.import(file, calendar)
CSV.foreach(file.path, headers: true) do |row|
stock = find_by(id: row['id']) || new
row_hash = row.to_hash.slice(*CSV_HEADER.keys)
stock.attributes = row_hash.transform_keys(&CSV_HEADER.method(:))
stock['calendar_id'] = calendar
stock.save
end
end

CSV_HEADER = {
'書店コード' => 'store_code',
'地区名' => 'district',
'書店名' => 'store_name',
〜〜省略〜〜
 
'ISBNコード' => 'isbn',
}.freeze
end

複雑な作業をコントローラーでさせる訳にはいかないので、モデルに記述します。

モジュールに書き出して呼び出すと言う方法もある様です。

ポイントは3つ

  • ヘッダーが日本語問題対策
  • CSV.foreachのオプションで指定しているもの
  • 親のidを別途登録

順に説明していきます。

補足説明6-1.ヘッダーが日本語問題対策

ヘッダーが日本語なので、そのままだとattributesに入れる事ができない・・・

まあ、ぶっちゃけマイグレーションを日本語のカラム名にして

csvファイルをそのまま取り込むことはできました。

ただし、localhos上だけですが。

(試していないのですがheroku上でも大丈夫なのではないでしょうか。)

ただ、ここで楽をしてしまうと、成長止まるかなとも思いチャレンジです。

ヘッダーの項目名とテーブルのカラム名のハッシュを

CSV_HEADER」として定義しておいて、

「transform_keys」でカラム名に一致するように入れ替えます。

これで、modelのattributesに対応します。

補足説明 6-2.各記述の説明

「CSVforeach」で一行ずつ解析していくのですが

headers: true:ヘッダーを読み飛ばしてくれます!

stock = find_by(id: row['id']) || new

今回のCSVファイルにはIDの表記がないので

もし合った場合はそのレコードを呼び出し、ない場合は新しく作成。

row_hash = row.to_hash.slice(*CSV_HEADER.keys)
stock.attributes = row_hash.transform_keys(&CSV_HEADER.method(:))

CSV_HEADERを基にして、hashに変換する。

stock['calendar_id'] = calendar

最後に、親のidを追加

ふ〜・・・。やっとできました!

7.残る課題

やっとこさ出来上がってはみたのですが

記述方法が悪いのかなんだか取り込みスピードが遅い気がするのですよね。

herokuだともっと遅い気がします。

この辺を勉強して、早く処理できる様になったらまたブログにします!

8.参考にさせていただいたサイト様

[Rails6]CSVインポート処理を作る|ota|note

【Rails】CSVインポート機能の実装 - Qiita

どちらの方にも足向けて眠れません。

自分なりの解釈も入れてはいますがほぼ上記の方々の書かれているとおりです。

少しづつでも理解して行って、どなたかの助けになっていたらいいな〜。