Firestoreの全ドキュメントを削除する処理

Firestoreの全ドキュメントを削除する処理

September 15, 2021

例えばローカルのエミュレータ環境の Firestore へシードデータを流す際、シーディングの初期化処理としてデータを全削除したいというシチュエーションはよくあると思います。

ということでそれを実現するコードを用意しました。

Firestore ドキュメント全削除処理(TypeScript) #

// Initialize
import admin from 'firebase-admin';
import serviceAccount from './******-adminsdk.json';

admin.initializeApp({ credential: admin.credential.cert(serviceAccount as admin.ServiceAccount) });

// Delete all authentication users.
const auth = admin.auth();

(async () => {
  const users = await auth.listUsers(1000);
  await Promise.all(users.users.map(async (user) => await auth.deleteUser(user.uid)));
})();

// Delete all documents.
const db = admin.firestore();
type DocumentReference = FirebaseFirestore.DocumentReference;

const document = [
  ['colors'],
  ['foods', 'vegetables'],
  ['countries', 'states', 'cities'],
  ['companies', 'departments', 'teams', 'employees'],
];

(async () => {
  const deepDelete = async (collections: string[], parentRef?: DocumentReference) => {
    const collectionName = collections.shift();
    if (!collectionName) return;
    const parent = parentRef ?? db;
    const snapshots = await parent.collection(collectionName).get();
    await Promise.all(
      snapshots.docs.map(async (doc) => {
        const docRef = parent.collection(collectionName).doc(doc.id);
        await docRef.delete();
        if (collections.length) await deepDelete([...collections], docRef);
      })
    );
  };

  await Promise.all(document.map(async (collections) => await deepDelete(collections)));
})();

解説 #

見ればわかるよ、と言われてしまうかもしれませんが、念のためコードを上から順に、処理のまとまりごとに解説していきます。

1.初期化処理 #

// Initialize
import admin from 'firebase-admin';
import serviceAccount from './******-adminsdk.json';

admin.initializeApp({ credential: admin.credential.cert(serviceAccount as admin.ServiceAccount) });

初期化処理に関して解説することはないですね。決まった形を書くだけです。

2.Authentication のユーザを全削除 #

// Delete all authentication users.
const auth = admin.auth();

(async () => {
  const users = await auth.listUsers(1000);
  await Promise.all(users.users.map(async (user) => await auth.deleteUser(user.uid)));
})();

ユーザを 1,000 件取得し、その後それらをすべて削除しています。listUsers() が取得可能な上限が 1,000 件です。私の場合はシードデータで 1,000 件以上のユーザを作ることがないため上記で満足しています。ユーザがそれ以上に存在する場合は上記処理を複数回動かせば OK です。その場合、ユーザがまだ残っていれば改めて同じ動きをするに再帰的に処理すれば良いと思います。適宜カスタマイズしてください。

3.Firestore のドキュメントを全削除 #

// Delete all documents.
const db = admin.firestore();
type DocumentReference = FirebaseFirestore.DocumentReference;

const document = [
  ['colors'],
  ['foods', 'vegetables'],
  ['countries', 'states', 'cities'],
  ['companies', 'departments', 'teams', 'employees'],
];

最初に全てのコレクションを定義します。配列の1番目にルートのコレクションを書き、そのサブコレクションを2番目以降に続けます。上記は、以下のコレクション/サブコレクション構造を表しています。

├ colors
├ foods
│  └ vegetables
├ countries
│  └ states
│     └ cities
└ companies
   └ departments
      └ teams
         └ employees

上記定義をもとに、次の削除処理を行います。

(async () => {
  const deepDelete = async (collections: string[], parentRef?: DocumentReference) => {
    const collectionName = collections.shift();
    if (!collectionName) return;
    const parent = parentRef ?? db;
    const snapshots = await parent.collection(collectionName).get();
    await Promise.all(
      snapshots.docs.map(async (doc) => {
        const docRef = parent.collection(collectionName).doc(doc.id);
        await docRef.delete();
        if (collections.length) await deepDelete([...collections], docRef);
      })
    );
  };

  await Promise.all(document.map(async (collections) => await deepDelete(collections)));
})();

先ほど定義した配列を利用して deepDelete() でドキュメントを削除していきます。バクっと説明すると、deepDelete() の中で自身を再帰的に呼び出して削除処理を行なっています。

ポイント:Batch 処理を使わずに Promise.all() で処理 #

一括でドキュメント更新を行う場合、まずは batch() を使った書き方が思い浮かびますが、処理速度は Promise.all() の方が早いです。今回のような使い方であれば、後者の方がより良いと思います。

FYI

TL;DR: The fastest way to perform bulk date creation on Firestore is by performing parallel individual write operations.

There are three common ways to perform a large number of write operations on Firestore.

  1. Perform each individual write operation in sequence.
  2. Using batched write operations.
  3. Performing individual write operations in parallel.

Writing 1,000 documents to Firestore takes:

  1. ~ 105.4s when using sequential individual write operations
  2. ~ 2.8s when using (2) batched write operations
  3. ~ 1.5s when using parallel individual write operations

補足:1秒間あたりの削除可能数制限 #

Firestore の仕様として1秒間あたりの書き込み回数には上限があります。

書き込みとトランザクション

  • 上限:データベースあたりの最大書き込み回数/秒
  • 詳細:10,000(最大 10 MiB/秒)

私の場合は 10,000 件以上のシードデータを用意することがないためこの制限は気になりませんが、例えば本番運用している Firestore をリセットするために今回のコードを利用する場合などはこの制限を考慮した方が良さそうです。

この対応を行うのは簡単で、 Promise.all() の処理を、 for await...of に書き換えるだけです。

for await…of での処理 #

一例として、先に示したコードの deepDelete() 関数内の Promise.all()for await...of に変えた例が以下です。

// Promise.all() 版
await Promise.all(
  snapshots.docs.map(async (doc) => {
    const docRef = parent.collection(collectionName).doc(doc.id);
    await docRef.delete();
    if (collections.length) await deepDelete([...collections], docRef);
  })
);

// for await...of 版
for await (doc of snapshots.docs.map) {
  const docRef = parent.collection(collectionName).doc(doc.id);
  await docRef.delete();
  if (collections.length) await deepDelete([...collections], docRef);
}

Promise.all() だと、削除処理を並列で発行する動きをしますが for await...of では順列処理になります。

その結果、処理スピード上1秒間に 10,000 件もリクエストが送られることはなくなり、上限に達する心配もありません。

以上 #

この記事がお役に立てば幸いです!

追記 #

この記事を投稿した後になって知ったのですが、firebase-tools は CLI からだけでなく、通常のパッケージのように import してコマンドを呼び出すこともできるようなので、こちらを利用するのもありかもしれませんね。

https://firebase.google.com/docs/firestore/solutions/delete-collections

使用する Cloud Functions の関数用に独自の再帰的な削除ロジックを実装する代わりに、Firebase コマンドライン インターフェース(CLI)の firestore:delete コマンドを利用できます。firebase-tools パッケージを使用すると、Firebase CLI の任意の関数を Node.js アプリケーションにインポートできます。