(Swift)FirebaseのCloud Functionsを使用し、AppからSubCollection及びDocumentsを再帰的削除する方法


初めてCloudFunctionsを使用し、SubCollectionを削除しようとしたところハマったので備忘録として記事に残します。以下のコードにご指摘等ありましたらコメントください。

環境

Xcode:Version 11.5
Swift 5.0

この記事はこんな人用です:

  1. iOSアプリを開発中
  2. FireStoreを使用している。
  3. 既にCloudFunctionsの環境を構築済み
  4. FirebnaseAuthまたはUIを使用している。

問題

FirestoreDBに保存しているデータを削除するとき、AppからDelete処理を行なっていたが、
Collection/Document/SubCollection1/"Document"/SubCollection2/Document
のデータ構造を持つデータの"Document"を削除した時に、SubCollection2以下が削除されずに残ったままになっていることがわかった。

やりたかったこと

TableViewでデータのdocument削除処理を行った時に、SubCollection2以下を"Document"を削除する処理で、まとめて削除する方法はないものか???

Firebaseの公式資料を見てわかったこと

ドキュメントを削除しても、そのサブコレクション内のドキュメントは削除されない!
コレクション ツリー全体を安全かつ効率的に削除する Cloud Functions 関数の記述は可能!!

“Cloud Firestore でのデータの削除は、特にリソースが制限されるモバイルアプリからは、以下の理由により、正しく行えない場合があります。
* コレクションをアトミックに削除するオペレーションが存在しない。
* ドキュメントを削除しても、そのサブコレクション内のドキュメントは削除されない。
* ドキュメントに動的なサブコレクションが存在する場合は、指定されたパスでどのデータを削除すればよいかを認識するのが困難になる可能性がある。
* 500 を超えるドキュメントのコレクションを削除するには、複数回の書き込みバッチ オペレーションまたは数百回の単純削除が必要である。
* 多くのアプリでは、エンドユーザーにコレクション全体を削除する権限を付与するのは適切ではない。
ただし、コレクション全体やコレクション ツリー全体を安全かつ効率的に削除する Cloud Functions 関数の記述は可能です。”
https://firebase.google.com/docs/firestore/solutions/delete-collections#java

Cloud Functionsの実装

公式サイトの動画インストラクションに従い、CloudFunctionsの環境構築。
https://firebase.google.com/docs/functions/get-started

ハマりポインント1:Firebase CLIの設定

生成したトークンをどこに設定したらいいかわからなかった。

$ firebase login

//トークンの生成

$ firebase login:ci.

//生成したトークンをConfigファイルに設定
$ firebase functions:config:set fb.token =“FIREBASE TOKEN”

ハマりポインント2:CloudFunctionに書くコード

公式Documentで紹介されているコードをそのまま使うことができなかったため変更)

変更部分:
1.const firebase_tools = require("firebase-tools")
公式Documentのままのコードだと、await firebase_tools.firestore.deleteでエラーが発生するため。

2.(!(context.auth?.uid)
Appでユーザーがログインしていれば削除を実施できるように。

3.const path = data;
App内のコードでCouldFunction関数をコール時にDocumentReferenceを引数でそのまま渡そうとしたところエラーが発生したたため、App内のコードでpathを直接引数に。
https://firebase.google.com/docs/firestore/solutions/delete-collections#java

変更後のコード:

const firebase_tools = require("firebase-tools");

exports.recursiveDelete = functions
  .runWith({
    timeoutSeconds: 540,
    memory: '2GB'
  })
  .https.onCall(async (data, context) => {
    // Only allow admin users to execute this function.
    if (!(context.auth?.uid)) {
      throw new functions.https.HttpsError(
        'permission-denied',
        'Must be an authorized user to initiate delete.'
      );
    }
    const path = data;

    console.log(
      `User ${context.auth.uid} has requested to delete path ${path}`
    );

    // Run a recursive delete on the given document or collection path.
    // The 'token' must be set in the functions config, and can be generated
    // at the command line by running 'firebase login:ci'.
    await firebase_tools.firestore
      .delete(path, {
        project: process.env.GCLOUD_PROJECT,
        recursive: true,
        yes: true,
        token: functions.config().fb.token
      });

    return {
      path: path 
    };
  });

ハマりポインント3:AppからCloudFunctionを呼び出す

作成した関数をApp内でどうやって呼び足したらいいか、パスを渡したらいいかわからない・・公式DocumentにSwiftのコードだけ書いていない・・・・

参考コード:


       //削除したいDocumentのパス
        let documentRef: DocumentReference =  Firestore.firestore().collection("your collection name").document("your document ID").collection("your collection name").document("your document ID")


       //CloudFunction内の関数を呼び出すためしょり
        let deleteFn = functions.httpsCallable("recursiveDelete")
        deleteFn.call(documentRef.path){ (result, error) in
            if let error = error as NSError? {
                if error.domain == FunctionsErrorDomain {
                    let code = FunctionsErrorCode(rawValue: error.code)
                    let message = error.localizedDescription
                    let details = error.userInfo[FunctionsErrorDetailsKey]

                    print("User has requested to delete:\(documentRef.path) and failed with error; code:\(code), message:\(message),details:\(details)")
                }
                //Delete処理失敗

            }else{
                //Delete処理成功
                print("User has requested to delete \(documentRef.path) and succeeded")

            }
        }