Firestoreのテストデータ投入をGASで楽にする

# Firestoreへのテストデータ投入を楽にしたい！

## Firestoreの管理画面は使いづらい
Firestore、気楽に開発出来て大変便利ですよね。無料で始められるのも中小企業にとってありがたいところです。晴れて正式版になったのもウレシイ。
でも、開発時これはキツイな・・・と思ったのが、テストデータの投入なんです。
一件、二件程度なら別に構わないんですが、フィールドを多数持つようなレコードを大量に生成したいときは、管理画面からのデータ投入なんて不可能です。（そもそもそういうことを意図した画面ではないはずなので、Firestoreが劣っているということではないです）

## SpreadSheet&GASが良さそう
既にqiitaでも先人の方たちがその足跡（[GAS：SpreadSheetに登録したデータをFirestoreに送信する](https://qiita.com/masa-321/items/86e356b02dcf5b840595)　や、[firestoreにgoogle app script](https://qiita.com/muzou/items/ab215e6e9da93926600e)、　[【書籍管理シリーズ】GASとFireBase(Firestore)を連携させるよ！](https://qiita.com/h_plum/items/f6ba16931f955d33158e)など）を残してくれているように、Spreadsheet＆GASでfirestoreにデータを投入できると大変楽ができそうです。
先の先人の皆様の記事ですでに事足りてる感はあるのですが、FirestoreもGASもほぼ初めての私が実施したところそれなりにハマったので、備忘録的に記事にしてみた、というのが本稿の主題です。

## やりたいこと
* Excel感覚でテストデータを作成・管理したい（Spreadsheetで要件満たせる）
* Firestoreにテストデータを投入したい（コレがやりたい）
* データの更新、削除などは今は考えない。（とりあえずデータ入れること主眼）

## 早速やっていきましょう
ちょっと最近記憶力がアレなので文字だけの手順だと同じことが後で出来なくなる事情があり、画像多めにしておきます。

### その1.　準備編
まずはCloud Firestoreで適当にプロジェクトを用意していきます。当然すでにプロジェクトがある場合はスキップしてください。
次に、[GoogleAPIsの管理画面](https://console.developers.google.com/projectselector/iam-admin/serviceaccounts)を開き、左上にあるプロジェクト選択でfirestoreのプロジェクトを選択したうえで、サービスアカウントの作成を行っていきます。
![2019-04-09_20h15_53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221091/18e46f4b-a2f0-c05f-f145-b4a2821f0e2f.png)

名前などは適当に。重要なのはただ一点で、サービスアカウントの権限に「Cloud Datastoreオーナー」を指定すること。
![2019-04-09_20h32_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221091/0f6f16fc-f16b-87e8-ceba-9a5831e23c88.png)

作成が完了したら「キーの作成」にてJSONを選択して鍵情報をダウンロードしておきます。
![2019-04-09_20h18_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221091/9adb0cab-433f-02bd-0028-b71771765e96.png)

次にSpreadSheetの方も準備しておきましょう。
こちらも適当にドキュメント用意しておきます。
![2019-04-09_20h11_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221091/9385633b-1872-8119-abcd-9752d9a18ed1.png)



GASのスクリプトエディタを開きましょう。「ツール」->「スクリプトエディタ」で開けいます。
スクリプトエディタが開いたら、「リソース」->「ライブラリ」を開きます。
「ライブラリの追加」に魔法のコード：1VUSl4b1r1eoNcRWotZM3e87ygkxvXltOgyDZhixqncz9lQ3MjfT1iKFw
を、[grahamearley](https://github.com/grahamearley/FirestoreGoogleAppsScript)さんへの感謝をしつつ入力しましょう。

![2019-04-09_20h25_35 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221091/48f11234-a3a8-48f1-5fc4-c3276f35ae31.png)

バージョンはたぶん最新を選んでおけば間違いないです。
これで「保存」を押せばなんか許可画面が出ます。心変わりしてなければ許可をしましょう。
![2019-04-09_20h26_17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221091/64eb94b8-1c09-5c7a-2358-28bac2f093ce.png)

これで準備は完了です。簡単ですね。
私は一度やったこの手順が二回目にできなくて1時間ほど苦しみましたが、これからはこの記事があるのでもう大丈夫です。

### その2.　とりあえず使ってみる
まずは単純に使ってみましょう。Script内で単純なデータを用意して、それをFirestoreに無造作に投げ込んでみて相手の出方を見てみます。

```
function firstCreateData() {
  var email = "(dlしたJSONにあるclient_emailの値)";
  var key = "(dlしたJSONにあるprivate_keyの値。-----BEGIN PRIVATE KEY-～END PRIVATE KEY-----\nまで全部入れます)";
  var projectId = "(dlしたJSONにあるproject_idの値)";
  
  var firestore = FirestoreApp.getFirestore(email, key, projectId);
  
  // 登録データ
  const data = {
    "name": "test!"
  }
  
  firestore.createDocument("FirstCollection", data);
}
```

実行してみましょう。

![2019-04-11_21h13_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221091/11aff459-c8e1-b760-deb9-80cc0905cd73.png)

おお、入ってますね。
とりあえず無事疎通は完了です。

### その3.　SpreadSheetのデータをFirestoreに投入する。
ようやく主題です。
今回は以下のようなデータを投入することを目指します。
この形式では、1行目が登録時のフィールド名、2行目がフィールドの型で、3行目から登録対象データとなっています。まあこの辺りはお好みで。
![spreadsheet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221091/51cd1287-7869-356a-3086-5148dbbf2242.png)

まあ、最低限のUser情報といったところですが、フィールドやレコードは随時増やせばいいので、一旦はこれでやりましょう。

こういうデータの取り扱いは、それこそVBAなどでも数多書かれてきたと思います。
GASならではの注意点としては、最大実行時間が限られる（6分？）のため、あまり効率の悪い処理はしたくないといったところでしょうか。
ExcelにおけるVBAでよく見かける、セルを一つ一つ読んでいくやり方が最悪手のようなので、とりあえずそれを避け、Firestoreへの登録単位である1レコード（行)単位での読み込みをするようにしておきます。
また、本格的に利用するならシート名やデータ範囲の設定は動的にしておくべきでしょうが、ここでは目をつぶります。そのうち直すかもしれませんが。

```
// カラム名定義
var FIELD_ROW_START = 1;
var FIELD_COL_START = 1;
// 型
var FIELD_TYPE_ROW = 2;
// データの開始行・列
var DATA_ROW_START = 3;
var DATA_COL_START = 1;

// フィールド情報key
var KEY_I_NAME = 'implName'; //実装名
var KEY_TYPE = 'type'; // 型

// 対象シート
var SHEET = "Users";

function myFunction() {
  // firestore接続情報
  var email = "(dlしたJSONにあるclient_emailの値)";
  var key = "(dlしたJSONにあるprivate_keyの値。-----BEGIN PRIVATE KEY-～END PRIVATE KEY-----\nまで全部入れます)";
  var projectId = "(dlしたJSONにあるproject_idの値)";
    
  // 対象シート
  var sheet = getSheet(SHEET);
  // コレクション名=シート名とする
  var collectionName = sheet.getName();
  
  // 入力データ領域でループ
  var rowEnd = sheet.getDataRange().getLastRow();
  
  for(var i = 0; i <= rowEnd - DATA_ROW_START; i++) {
    // 一行ずつ読み込み&登録
    var data =  buildOneRecord(sheet, i);
    var docId = '';//docIdは自動生成
    
    var firestore = FirestoreApp.getFirestore(email, key, projectId);
    var path="";
    if(docId != '') {
      // docIdを指定したいときのため
      path = "/" + docId;
    }
    // 1レコード登録
    firestore.createDocument(collectionName + path, data);
  }
}

/**
* 1レコードの読み込み
* sheet : 対象シート
* dataNumber: 取り込みデータの行インクリメント数
*/
function buildOneRecord(sheet, dataNumber) {

  // データ領域の最終列
  var colEnd = sheet.getDataRange().getLastColumn();
  
  var ret = {};
  //フィールド情報を取得しておく
  var fields = getFieldsInfo(sheet);
  Logger.log(dataNumber);
  //一括で読み込み
  var rec = sheet.getSheetValues(DATA_ROW_START + dataNumber, DATA_COL_START, 1, colEnd - DATA_COL_START + 1);
  Logger.log(rec);
  var r = {}
  for(var i = 0; i < fields.length; i++){
    var field = fields[i];
    if(field[KEY_TYPE] == 'array') { 
      // TODO:配列の時
      // 
    } else if(field[KEY_TYPE] == 'map') {
      // TODO:mapの時
      //
    } else if(rec[0][i] && rec[0][i] != '') {
      // その他の場合は値があるもののみ格納する 
      r[field[KEY_I_NAME]] = rec[0][i];;
    }
  }
  return r;
}

/**
* フィールド情報を取得する
* @sheet: シート
*/
function getFieldsInfo(sheet) {
  var colEnd = sheet.getDataRange().getLastColumn();
  var fields = sheet.getSheetValues(FIELD_ROW_START, FIELD_COL_START, FIELD_TYPE_ROW - FIELD_ROW_START + 1, colEnd);
  
  var f = [];
  for(var i = 0; i < fields[0].length; i++) {
    var logicalName = null;
    var implName = null;
    var type = null;

    if(fields[0][i] != '') {
      // フィールド名
      fieldName = fields[0][i];
      // 型
      fieldType = fields[1][i];
      
      // 戻り値用値の整形
      var m = {}
      m[KEY_I_NAME] = fieldName;
      m[KEY_TYPE] = fieldType;
      f.push(m);
    }
  }
  return f;
}

/**
* 処理対象のシートを取得する
* @sheetName: 対象シート名
*/
function getSheet(sheetName) {
  if(getSheet.sheet) { return getSheet.sheet; }
  getSheet.sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName); 
  return getSheet.sheet;
}
```

コードの中でTODOとしているのでもろバレですが、本スクリプトはmap形式やarray形式データの投入には対応していません。というか、実際に作成したスクリプトでは対応させたのですが、そもそもExcelのような表形式のUIでレコード内に入れ子となったmapやarrayって3次元以上の情報となってしまうので、どうデータを扱うかの決めがないと厳しく、汎用的にしづらいと思ったのであえて消しました。必要な方は自作してください。

あと、docIdは自動生成のみを想定しています。createDocumentをupdateDocumentにして、docIdもSpreadSheet上で管理するようにすれば更新にもうまいこと対応できそうですが、そこは後々必要になったら対応しようかといったところです。

一応実行するとこんな感じで複数レコードが登録されるはずです。
![2019-04-11_21h28_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221091/ac7a4ce4-590f-350f-5db9-9acf9fb53a01.png)

量にもよりますが、6分の壁を超えるような大量レコードの投入の際には分割するなどよしなにしていただければ良いかと思います。