FirestoreにCSVをImportする方法
現時点(2018年11月時点)ではFirestoreは未だベータ版な上に公式がCSVのimport / exportに対応していないため、自分でimport / exportをサポートするアプリケーションを作らないといけない。この記事では簡易的に作った、FirestoreにCSVのimport / exportをサポートするアプリケーションについて書いて行きます。
1. 準備する
1-1. モジュールのインポートと初期設定
Import / Export用のアプリケーションにはNode.jsを使います。
それではアプリケーションを作るディレクトリへ移動し、以下のモジュールをターミナルからインストールしましょう。
1 |
npm install csv-parse firebase-admin fs |
必要なモジュールをインストールしたら、package.jsonを作りましょう。
1 |
npm init |
package.jsonに記載する情報を聞かれるので、順に答えて行ってください。以下、参考です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "name": "firestoreimportexport", "version": "1.0.0", "description": "supports csv import and export for firestore", "main": "index.js", "dependencies": { "csv-parse": "^3.2.0", "firebase-admin": "^6.1.0", "fs": "^0.0.1-security" }, "devDependencies": {}, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Orange", "license": "ISC" } |
1-2. Firebase ServiceAccountの用意
Firebase Admin SDKを使うには、ServiceAccountのjsonファイルが必要になります。ServiceAccountファイルは、Firebaseの管理画面の「プロジェクトの設定」>「サービスアカウント」からダウンロード出来ます。ダウンロードしたファイルをさっき作成したpackage.jsonと同じ階層に入れてください。
2. コーディング
2-1. Adminの初期化
準備は整いましたので、index.jsのファイルを作成し、firebase adminの初期化を行いましょう。初期化のコードはFirebase 管理画面の「プロジェクト設定」>「サービスアカウント」のAdmin SDK構成スニペットからコピペ出来ます。
1 2 3 4 5 6 7 8 |
var admin = require("firebase-admin"); var serviceAccount = require("path/to/serviceAccountKey.json"); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), databaseURL: "https://[projectID].firebaseio.com" }); |
2-2. インポートの流れ
CSVデータをインポートする流れとしては:
- CSVファイルをパースする
- パースしたCSVデータを forEachで回して空の配列の中にkey-value形式で入れていく。
- トランザクションを作成し、インポートする。
それではindex.jsに以下を追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const db = admin.firestore() const fs = require('fs') const csvSync = require('csv-parse/lib/sync') const file = 'hoge.csv' //インポートしたいcsvファイルをindex.jsと同じ階層に入れてください let data = fs.readFileSync(file) //csvファイルの読み込み let responses = csvSync(data)//parse csv let objects = [] //この配列の中にパースしたcsvの中身をkey-value形式で入れていく。 responses.forEach(function(response) { objects.push({ _id: response[0], hogehoge: response[1], fugafuga: response[2] }) }, this) objects.shift(); //ヘッダもインポートされてしまうから、配列の一番最初のelementは削除します。 |
*一番最初のkey-valueは _idにしてください。これはfirestoreのdocument idに使うので、これが無い場合、データの更新が行えず毎回新しいdocumentが生成されてしまいます。詳しくは次のセクションで説明します。
例えば、以下のようなCSVがあった場合、objects配列の中身はこうなるイメージです:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
objects = [ { _id: "id1", hogehoge: "hoge", fugafuga: "fuga" }, { _id: "id2", hogehoge: "hoga", fugafuga: "fuge" }, { _id: "", hogehoge: "hogehoga", fugafuga: "fugafuge" } ] |
現在、index.jsは以下のようになっているはずです:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
var admin = require("firebase-admin"); var serviceAccount = require("path/to/serviceAccountKey.json"); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), databaseURL: "https://[projectID].firebaseio.com" }); const db = admin.firestore() const fs = require('fs') const csvSync = require('csv-parse/lib/sync') const file = 'hoge.csv' //インポートしたいcsvファイルをindex.jsと同じ階層に入れてください let data = fs.readFileSync(file) //csvファイルの読み込み let responses = csvSync(data)//parse csv let objects = [] //この配列の中にパースしたcsvの中身をkey-value形式で入れていく。 responses.forEach(function(response) { objects.push({ _id: response[0], hogehoge: response[1], fugafuga: response[2] }) }, this) objects.shift();//ヘッダもインポートされてしまうから、配列の一番最初のelementは削除します。 |
2-3. トランザクション
Firestoreのdocument idはdocumentの中には含まれず、一つ上の階層にあります。jsonで表すと↓みたいな感じです:
1 2 3 4 |
document_id : { documentKey : documentValue, documentKey2: documentValue2 } |
*Cloud Firestoreのデータモデルを詳しく知りたい方はこちら
ですので、CSVの中にidを含めたとしても、実際のdocument idは別で自動生成されてしまいます。このままだと使い勝手が悪く、更新も行えないため以下の方法を取っています:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
return db.runTransaction(function(transaction){ return transaction.get(db.collection('collectionName')).then(doc => { objects.forEach(function(object){ if(object["_id"] != ""){ let id = object["_id"] delete object._id transaction.set(db.collection('collectionName').doc(id), object) }else{ delete object._id transaction.set(db.collection('collectionName').doc(), object) } }, this) }) }).then(function(){ console.log("success") }).catch(function(error){ console.log('Failed', error) }) |
分解しながら見ていきましょう。先ほど例にあげたCSVの一番最初のelementで例えていきます。
objects.forEachでobjects配列を回して、もしCSVの_id カラムが空白じゃない場合:
1 2 3 4 5 |
if(object["_id"] != ""){ let id = object["_id"] //id = "id1" delete object._id // _id : "id1" を削除する transaction.set(db.collection('collectionName').doc(id), object) } |
「id1」というdocumentに、(無い場合は生成されます)
1 2 3 4 |
{ hogehoge: "hoge", fugafuga: "fuga" } |
↑の内容を書き込み、更新する処理を行なっている。
続いて、先ほど例にあげた最後のelementで例えていきます。
もしCSVの_idカラムが空白の場合:
1 2 3 4 |
else{ delete object._id // _id : "" を削除する transaction.set(db.collection('collectionName').doc(), object) } |
document id が指定されていないため、新しくdocument が生成され、
1 2 3 4 |
{ hogehoge: "hogehoga", fugafuga: "fugafuge" } |
↑の内容をdocumentに書き込み、保存します。Document idは自動で生成されます。
最終的にindex.jsは以下のようになっているはずです:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
var admin = require("firebase-admin"); var serviceAccount = require("path/to/serviceAccountKey.json"); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), databaseURL: "https://[projectID].firebaseio.com" }); const db = admin.firestore() const fs = require('fs') const csvSync = require('csv-parse/lib/sync') const file = 'hoge.csv' //インポートしたいcsvファイルをindex.jsと同じ階層に入れてください let data = fs.readFileSync(file) //csvファイルの読み込み let responses = csvSync(data)//parse csv let objects = [] //この配列の中にパースしたcsvの中身をkey-value形式で入れていく。 responses.forEach(function(response) { objects.push({ _id: response[0], hogehoge: response[1], fugafuga: response[2] }) }, this) objects.shift();//ヘッダもインポートされてしまうから、配列の一番最初のelementは削除します。 return db.runTransaction(function(transaction){ return transaction.get(db.collection('collectionName')).then(doc => { objects.forEach(function(object){ if(object["_id"] != ""){ let id = object["_id"] delete object._id transaction.set(db.collection('collectionName').doc(id), object) }else{ delete object._id transaction.set(db.collection('collectionName').doc(), object) } }, this) }) }).then(function(){ console.log("success") }).catch(function(error){ console.log('Failed', error) }) |
3. 使ってみる
- ターミナルで、index.jsがあるディレクトリに移動する
- インポートするcsvがindex.jsと同じ階層にある事を確認し、
1node index.js
とターミナルコマンドを打つ*csvは空の行や列を削除してください。
- successと出てきたら成功
4. おまけ:データの形式
2-2. でparseしたCSVのデータは全て文字列形式になっています。ですので、空のobjects 配列に入れる前に、必要に応じてデータ形式を変える必要があります。
例1) Intの場合:
1 2 3 4 5 |
objects.push({ _id: response[0], hoge: parseInt(response[1], 10) }) }, this) |
例2) Floatの場合:
1 2 3 4 5 |
objects.push({ _id: response[0], hoge: parseFloat(response[1]) }) }, this) |
例3) Arrayの場合:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
responses.forEach(function(response) { var hogeArray = [] //空の配列を生成 //split関数を使って、文字列形式になってしまっている配列を","でsplitして、空の配列に代入する if(response[1].slice(1,-1).split(",") != ""){ hogeArray = response[1].slice(1,-1).split(",") } objects.push({ _id: response[0], hoge: hogeArray, }) }, this) |
例4) GeoPointの場合:
1 2 3 4 5 |
objects.push({ _id: response[0], hoge: new admin.firestore.GeoPoint(Number(緯度), Number(経度)) }) }, this) |
以上、個人的に作ったFirestoreのCSV importをサポートする簡易的なアプリケーションでした。CSVカラムやデータ量から実用的かどうかが違ってきたり、もっと良い方法もあると思いますので、参考程度にして頂ければ幸いです。
追記:Exportの方にもご興味がある方はこちらの記事に書いていますので、よろしければご覧ください。