Help us understand the problem. What is going on with this article?

【Firebase】CLI 経由で Firestore に大量のテストデータを投入する

はじめに

アドベントカレンダー初投稿です!
業務で Firestore を利用したアプリ開発をしていて、ページネーションを実装するにあたって大量のテストデータを用意したくなったので勉強がてら実装してみました。。。

この投稿は Firebase Advent Calendar 2018 16日目の記事です

やりたいこと

  • firestore の users コレクションにテストデータを投入する
  • 各ドキュメントはランダムに生成されたデータが入っている
  • 多めの件数でも任意に指定して書き込めるようにする(最大10,000件)
  • テストデータだけを全件消去できる
  • 以上すべてをコマンドラインツールとして実現する(引数で制御したい)

環境構築

以下の前提で進める

  • firebase init でディレクトリを生成済
  • firebase-admin を使うために functions ディレクトリ内でコードを書く
  • TypeScript を使用
  • パッケージマネージャーは yarn

必要なパッケージをインストール

/functions
$ yarn add ts-node faker commander

typescript ファイルを作成

functions
└── cli
    ├── index.ts
    └── user.ts

注:functions としてデプロイするわけではないのでビルドの対象には含めなくてよい

CLI 実行用の run scripts を作成

package.json
{
  "scripts": {
    "cli": "ts-node cli/index.ts"
  }
}

モックデータ用関数の実装

  • ランダムなデータを作成するために faker を利用
  • 一括書き込みの上限が500件なので、再帰関数で500件ずつ書き込む
    参考:トランザクションと一括書き込み
  • 作成したモックデータのみ消せるように、isMock フィールドを用意した
user.ts
import * as admin from 'firebase-admin'
import * as faker from 'faker'

// 共通化するために Admin SDK の初期化は index.ts で行う
const db = admin.firestore()
const batchSize = 500

// 作成用関数
export const createUsers = async (n: number) => {
  const usersRef = await db.collection('users')

  // 再帰関数
  const excuteBatch = async (size: number) => {
    if (size === 0) {
      return
    }
    // 500件を超えないようにする
    const length = Math.min(size, batchSize)
    const batch = db.batch()
    for (let i = 0; i < length; i = i + 1) {
      const user = {
        firstName: faker.name.firstName(),
        lastName: faker.name.lastName(),
        createdAt: admin.firestore.FieldValue.serverTimestamp(),
        updatedAt: admin.firestore.FieldValue.serverTimestamp(),
        isMock: true // 削除時のクエリ用フィールド
      }
      batch.create(usersRef.doc(), user)
    }
    const results = await batch.commit()
    // 書き込みが成功した分を引いた残りの件数を代入
    await excuteBatch(size - results.length)
  }
  await excuteBatch(n)
}

// 削除用関数
export const deleteUsers = async () => {
  // モックデータのみを500件ずつ取得
  const query = await db
    .collection('users')
    .where('isMock', '==', true)
    .limit(batchSize)

  // 再帰関数
  const executeBatch = async () => {
    const snapshot = await query.get()
    if (snapshot.size === 0) {
      return
    }
    const batch = db.batch()
    snapshot.docs.forEach(doc => {
      batch.delete(doc.ref)
    })
    await batch.commit()
    await executeBatch()
  }
  await executeBatch()
}

削除用の関数の実装は公式ドキュメントを参考にした
Cloud Firestore からデータを削除する#コレクションを削除する

ドキュメントでは process.nextTick を使っているが、上記の実装では async/await で制御できているから要らない?(マサカリお待ちしています)

CLI ツールの実装

index.ts でそのまま呼び出して実行しても良かったが、CLI ツールとして実装した
commander というライブラリを利用(大したことはしないのでなんでも良い)
バージョン情報や、ヘルプコマンドなど必要なものはよしなに設定してくれたので中々良かった

index.ts
import * as admin from 'firebase-admin'
import * as program from 'commander'

// user.ts を import する前に Admin SDK を初期化する
admin.initializeApp()
admin.firestore().settings({ timestampsInSnapshots: true })

import { createUsers, deleteUsers } from './user'

program.version('1.0.0', '-v, --version')
program
  .command('user')
  .option('-d, --delete', 'delete only the created documents')
  .option('-n, --number <n>', 'A number of test documents', parseInt, 0)
  .description('create test user documents')
  .action(cmd => {
    if (cmd.number > 10000) {
      return console.error('The number must be 10000 or less')
    }
    const promise = cmd.delete ? deleteUsers() : createUsers(cmd.number)
    promise.then(() => console.log('Command has completed')).catch(console.error)
  })
program.parse(process.argv)

console.log('Firestore Mocking CLI')

テストデータ作成

初めに作成した run scripts 経由で実行する

$ yarn cli user -n 1000

yarn run v1.12.3
$ ts-node test/cli.ts user -n 1000
Firestore Mocking CLI
Command has completed
✨  Done in 6.71s.

テストデータ削除

$ yarn cli user -d

yarn run v1.12.3
$ ts-node test/cli.ts user -d
Firestore Mocking CLI
Command has completed
✨  Done in 5.73s.

おわりに

無料枠の制限にひっかからないように気をつけましょう…
2018/12/15 現在は以下の通りです

firebase_2018-12-14_23-09-07.png

そもそも一括書き込みやトランザクションは1回の実行で1件?500件?どちらなんでしょう:thinking:

[追記]
@mono0926 さんよりコメントいただきました

500ドキュメント扱った場合は、いかなる方法でも500件のコストです。

書き込み件数の節約のためにバッチやトランザクションを使うことはできないということですね!

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away