🔥

Firestore でコレクションとサブコレクション含めをすべてのドキュメント削除する方法

2021/02/02に公開

はじめに

Cloud Firestoreでコレクションとサブコレクションを削除する方法についてのTipsです。

TL;DR

親ドキュメント削除しただけだと、コレクション、サブコレクションは削除されません。そのため、下記の方法を取る必要があります。

  1. ドキュメントを再帰的に削除する方法
  2. firebase-toolsを使用して削除する方法
    ※整合性がないため、部分的な削除が発生したケースを処理する必要があります。

事前準備

下記のようなコレクションが存在するとして、comapnies/ABC以下のドキュメント、サブコレクションを削除したいというユースケースを考えていきます。

少し分かりづらいですが、-: コレクション, +: ドキュメント, : フィールドを表しています。

- companies
  + ABC
    ・owner: 鈴木 一郎
    ・phoneNumber: 13
    - divisions
      + development
        ・leader: 鈴木 一郎
        ・name: 開発部署
        - teams
          + backend
            ・leader: 鈴木 一郎
            ・name: バックエンド
          + frontend
            ・leader: 鈴木 一郎
            ・name: フロント
          + infra
            ・leader: 鈴木 一郎
            ・name: インフラ
      + sales
        ・leader: 鈴木 一郎
        ・name: 営業
        - teams
          + corporate
            ・leader: 鈴木 一郎
            ・name: 開発部署
          + person
            ・leader: 鈴木 一郎
            ・name: 開発部署
  + DEF
    ....
こちらはテストデータ作成用のスクリプトです。
index.ts
import * as admin from "firebase-admin";

const serviceAccount = require("./serviceAccountKey.json");

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

(async () => {
  const db = admin.firestore();

  for (const company of ["ABC", "DEF"]) {
    const companyRef = db.collection("companies").doc(company);
    await companyRef.set({phoneNumber: 123, owner: "鈴木 一郎"});

    const divisionsRef = companyRef.collection("divisions");

    const developmentRef = divisionsRef.doc("development");
    await developmentRef.set({name: "開発部署", leader: "鈴木 一郎"});
    await developmentRef.collection("teams").doc("frontend").set({name: "フロント", leader: "鈴木 一郎"});
    await developmentRef.collection("teams").doc("backend").set({name: "バックエンド", leader: "鈴木 一郎"});
    await developmentRef.collection("teams").doc("infra").set({name: "インフラ", leader: "鈴木 一郎"});

    const salesRef = divisionsRef.doc("sales");
    await salesRef.set({name: "営業", leader: "鈴木 一郎"});
    await salesRef.collection("teams").doc("corporate").set({name: "開発部署", leader: "鈴木 一郎"});
    await salesRef.collection("teams").doc("person").set({name: "開発部署", leader: "鈴木 一郎"});
  }
})().catch((e) => {
  console.log(e);
});

再帰的に削除する方法

公式ドキュメントにある通り、FireStoreは親ドキュメントを削除しても、サブコレクションは削除されません。

例えば、await db.collection('companies').doc('ABC').delete(); で親ドキュメントを削除しても下記のようになります。

- companies
  + ABC ← Filedは削除されるが、サブコレクションは削除されない
    - divisions
      + development
        ・leader: 鈴木 一郎
        ・name: 開発部署
        - teams
          + backend
            ・leader: 鈴木 一郎
            ・name: バックエンド
          + frontend
            ・leader: 鈴木 一郎
            ・name: フロント
          + infra
            ・leader: 鈴木 一郎
            ・name: インフラ
      ...

ドキュメントは削除(Filedも)されますが、サブコレクション内のドキュメントは削除されません。
また、実体がなくなるため、 db.collection("companies").get() のようにしても、ABCは取得できません。

そのため、すべてのドキュメントを削除するにはサブコレクション内のドキュメントを削除していくことになります。
公式ドキュメントにも実装例がありますが、下記はサブコレクションの削除も含めた実装例をとなります。

import * as admin from "firebase-admin";

const serviceAccount = require("./../../serviceAccountKey.json");

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

const deleteDocumentRecursively = async (docRef: FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData>) => {
  const collections = await docRef.listCollections();

  if (collections.length > 0) {
    for (const collection of collections) {
      const snapshot = await collection.get();
      for (const doc of snapshot.docs) {
        await deleteDocumentRecursively(doc.ref);
      }
    }
  }

  await docRef.delete();
};

(async () => {
  const db = admin.firestore();

  // サブコレクション含め再帰的に削除する
  await deleteDocumentRecursively(db.collection("companies").doc("ABC"));
})().catch((e) => {
  console.log(e);
});

firebase-toolsを使用する方法

自前で再帰処理を実装せず、簡単に削除する方法がfirebase-toolsを使ったものとなります。
公式ドキュメントでもこちらを推奨しているようです。

本番環境でコレクションを削除するために推奨される方法については、コレクションとサブコレクションを削除するをご覧ください。

解決策: 呼び出し可能な Cloud Functions の関数を使用してデータを削除する にある実装例とほぼ同じ内容です。
ただ、型定義ファイルが存在しないため、TypeScriptを使う場合は型定義を書く必要が出てきます。

import * as admin from "firebase-admin";
import {firestore} from "firebase-tools";

const serviceAccount = require("./../../serviceAccountKey.json");

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

(async () => {
  await firestore.delete("companies/ABC", {
    project: [projectName],
    recursive: true,
    yes: true,
  });
})().catch((e) => {
  console.log(e);
});
firebase-tools.d.ts
declare module "firebase-tools" {
  export namespace firestore {
    function _delete(path: string, options: {project: string, recursive: boolean, yes: boolean, token?: string}): Promise<any>
    export {_delete as delete};
  }
}

しかし、この方法は制限事項 があります。

削除オペレーションがいつも同じように成功または失敗するという保証もないため、部分的な削除が発生したケースを処理する準備が必要です。

部分的な削除が発生する可能性があるため、すべて削除されたかの確認をする必要があります。
そこで使用するAPIが、runQuery となります。こちらを使用することでサブコレクション内含めてのドキュメント情報をすることが可能となります。
消し残しがある場合は再度、削除処理を行えば、整合性も担保可能です。

import {FirestoreClient} from "@google-cloud/firestore/build/src/v1/firestore_client";
import * as admin from "firebase-admin";

const serviceAccount = require("./../../serviceAccountKey.json");

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

(async () => {
  const client: FirestoreClient = new admin.firestore.v1.FirestoreClient();
  const stream = client.runQuery({
    parent: "projects/firebase-tutorial-suzuki/databases/(default)/documents/companies/ABC",
    structuredQuery: {
      limit: {
        // 取得件数。1件の指定で良いと思う
        value: 10,
      },
      from: [
        {allDescendants: true},
      ],
    },
  });

  let count = 0;
  stream.on("data", (response) => {
    if (response.document) {
      count++;
    }
    console.log(response);
  });
  stream.on("end", () => {
    if (count > 0) {
      console.log(`${count}件のドキュメント残ってるよ`);
    } else {
      console.log("サブコレクション含め消えているよ");
    }
  });
})().catch((e) => {
  console.log(e);
});

補足

firebase-tools(v9.3.0時点では) のdeleteも同じように、runQuery を使用してドキュメント一覧を取得してから削除しているようです。
https://github.com/firebase/firebase-tools/blob/3c0d4ed89d82117a6081be3b355438a49c01c795/src/firestore/delete.ts#L268

おわりに

Firestoreでサブコレクション内を含めたデータの削除は以外と手間なんですね...

Discussion