複数のプロジェクトのfirebase-adminを起動して、開発用に本番データの一部を移行する

開発している[Webサービス](https://tsundoku.site)で、本番データの一部を開発用に使いたいと思ったときの備忘録。
firebase-adminは複数のプロジェクトで使えるらしい。

開発用だけじゃなく、UIDを変更しながらのデータ移行などにも使えそう。

## ローカル環境でfirebase-adminを使うための準備

準備の部分は、以前まとめたこちらを参照ください〜
- [ローカルPCからfirebase-adminを使ってFirestoreを操作する(管理ツール) - くらげになりたい。](https://www.memory-lovers.blog/entry/2019/07/25/073000)

### ソースはこんな感じ。

```javascript
const admin = require("firebase-admin");  

// *** 移行元のプロジェクトの設定
const srcSA = require("./key/XXXXX.json");   // サービスアカウントの秘密鍵を取得
// firebase-adminの初期化
admin.initializeApp({ credential: admin.credential.cert(srcSA) }); 
const srcDB = admin.firestore();          // firestoreのインスタンスを取得

// ** 移行先のプロジェクトの設定
const destSA = require("./key/XXXXX.json");  // サービスアカウントの秘密鍵を取得
// ※同じadminを使って、別のAppを作成しないといけない※
// ※2つ目を初期化する場合は、"dest"など名前をつけないといけない※
const destAdmin = admin.initializeApp( { credential: admin.credential.cert(destSA) }, "dest");
const destDB = destAdmin.firestore();        // firestoreのインスタンスを取得

// ****************************
// * MAIN
// ****************************
async function main() {
  console.log(`***** START MAIN`);

  // 移行元からデータを取得
  const snaps = await srcDB.collection("data").get()
  const srcData = snaps.docs;

  for (let v of data) {
    try {
      // 移行先からデータを保存
      const destDataRef = destDB.collection("data").doc(v.id);
      await destDataRef.set(v.data());
    } catch (error) {
      console.errror(`Error: ${error}`, error);
    }
  }

  console.log(`***** END   MAIN`);
  process.exit(0);
}

main().then();
```

雛形はこんな感じ。

以前書いた以下の記事だと丸々移行するので、UIDが変わるとめんどくさいことになる。。

- [Firestoreのデータをgcloudを使ってバックアップ＆別のプロジェクトへインポートしてみる - Qiita](https://qiita.com/kira_puka/items/ef80b07347cfdd37f116)
- [Firestoreのデータを分析するための3つの方法とその注意点 - Qiita](https://qiita.com/kira_puka/items/98283e837f30209f23c8)

この方法であれば、

- whereで一部のだけに絞り込んだり、
- `set()`するときに、UIDを差し替えたり

できるので、開発用に使うにはよいかんじ。

##### 注意1: 2つ目を初期化する場合は別の名前に

そのまんまコピペで2つのfirebase-adminを作るとエラーに。。
2つ目を初期化する場合は、"dest"など名前をつけないといけないいけないらしい。。

 公式ドキュメントの「[複数のアプリを初期化する](https://firebase.google.com/docs/admin/setup#initialize_multiple_apps)」に書いてあった。

これで、2つのfirebase-adminが起動できるので、データ移行などもできそう(*´ω｀*)

##### 注意2: メモリを大量に使いそうな場合はオプションを指定

データが多く、メモリを大量に使いそうな場合は、
`--max_old_space_size`オプションを指定しないといけない。。

途中で失敗すると(読み込み/書き込み件数的に)悲しいことになるので、
大きめのサイズを指定しておくと良さそう。

```console
node --max_old_space_size=8192 index.js 
```

##### 注意3: 件数が多い場合は、limitを指定して再帰処理

大量のデータを一度に取得しようとすると、

> Deadline Exceeded error

が出て怒られるので、少しずつ実行するのがよいかんじ。

```javascript
const BATCH_SIZE = 300
async function moveData(lastItem) {
  // 移行元からデータを取得
  const colRef = srcDB.collection("data");
  let query = colRef.orderBy("createAt", "asc");
  if (lastItem != null) query = query.startAfter(lastItem.createAt);
  const snap = await query.limit(BATCH_SIZE).get();

  if (snap.docs.length === 0) return; // 0件なら終了する
  const srcData = snaps.docs;

  for (let v of data) {
    try {
      // 移行先からデータを保存
      const destDataRef = destDB.collection("data").doc(v.id);
      await destDataRef.set(v.data());
    } catch (error) {
      console.errror(`Error: ${error}`, error);
    }
  }

  // 最後の要素を渡して、そこから継続する
  await moveData(data[data.length - 1]);
}
// ****************************
// * MAIN
// ****************************
async function main() {
  console.log(`***** START MAIN`);

  await moveData(null);

  console.log(`***** END   MAIN`);
  process.exit(0);
}
```

##### 注意4: 複数の場所に反映する場合は、batchやtransactionを使う

途中でエラーになる場合もあるので、batchやtransactionを使うとよさそう。

公式ドキュメントだと、このあたり
- [トランザクションと一括書き込み  |  Firebase](https://firebase.google.com/docs/firestore/manage-data/transactions)

```javascript
  for (let v of data) {
    try {
      const batch = destDB.batch();

      // 移行先からデータを保存
      const destDataRef = destDB.collection("data").doc(v.id);
      batch.set(destDataRef, v.data());

      // 移行先からデータを保存: その2
      const destData2Ref = destDB.collection("data2").doc(v.id);
      batch.set(destData2Ref, v.data());

      // コミット
      await batch.commit();
    } catch (error) {
      console.errror(`Error: ${error}`, error);
    }
  }
```

ループ外で使うほうのもいい感じ。

ただ、一括で書き込めるドキュメント数は500までと制限があるので、
一度に書き込む範囲は適宜調整が必要。

> 1 回のトランザクションまたは一括書き込みでは、
> 最大500のドキュメントに書き込みを行うことができます。
> [トランザクションと一括書き込み  |  Firebase](https://firebase.google.com/docs/firestore/manage-data/transactions?hl=ja#security_rules_limits)

## まとめ

firebase-adminは複数のプロジェクトでも扱える。
部分的なデータ移行や一部を書き換えながらの移行もできる(*´ω｀*)

ただ、件数が多かったり、複数の書き込みがある場合は、いろいろ必要。。


## こんなのつくってます!!

積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました！
[積読ハウマッチ](https://tsundoku.site)は、Nuxt.js+Firebaseで開発してます!

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="200"/>

もしよかったら、遊んでみてくださいヽ(=´▽`=)ﾉ

要望・感想・アドバイスなどあれば、
公式アカウント([@MemoryLoverz](https://twitter.com/MemoryLoverz))や開発者([@kira_puka](https://twitter.com/kira_puka))まで♪