呼び出し可能な Cloud Functions の関数を使用してデータを削除する

このページでは、呼び出し可能な Cloud Functions の関数を使用してデータを削除する方法について説明します。この関数をデプロイすると、モバイルアプリやウェブサイトから直接呼び出して、ドキュメントやコレクションを再帰的に削除できます。たとえば、このソリューションを使用して、特定のユーザーがコレクション全体を削除できるようにすることができます。

コレクションを削除する他の方法については、データを削除するをご覧ください。

解決策: 呼び出し可能な Cloud Functions の関数を使用してデータを削除する

コレクション全体の削除は、リソースが制限されるモバイルアプリからは、以下の理由により実施が難しい場合があります。

  • コレクションをアトミックに削除するオペレーションが存在しない。
  • ドキュメントを削除しても、そのサブコレクション内のドキュメントは削除されない。
  • ドキュメントに動的なサブコレクションが存在する場合は、指定されたパスでどのデータを削除すればよいかを認識するのが困難になる可能性がある。
  • 500 を超えるドキュメントのコレクションを削除するには、複数回の書き込みバッチ オペレーションまたは数百回の単純削除が必要である。
  • 多くのアプリでは、エンドユーザーにコレクション全体を削除する権限を付与するのは適切ではない。

ただし、コレクション全体やコレクション ツリー全体を安全かつ効率的に削除する 呼び出し可能な Cloud Functions の関数の記述は可能です。下記の Cloud Functions の関数は、呼び出し可能関数として実装されています。つまり、ローカル関数の場合と同様に、モバイルアプリまたはウェブサイトから直接呼び出すことができます。

関数をデプロイしてデモを試すには、サンプルコードをご覧ください。

Cloud Functions 関数

以下の Cloud Functions 関数は、コレクションとその子孫すべてを削除します。

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

Firebase CLI では、Firestore REST API を使用して、指定したパスに格納されているドキュメントをすべて検索し、個別に削除します。この実装では、アプリの特定のデータ階層を認識しておく必要はありません。場合によっては、親がなくなって「孤立した」ドキュメントを見つけて削除することもあります。

Node.js

/**
 * Initiate a recursive delete of documents at a given path.
 *
 * The calling user must be authenticated and have the custom "admin" attribute
 * set to true on the auth token.
 *
 * This delete is NOT an atomic operation and it's possible
 * that it may fail after only deleting some documents.
 *
 * @param {string} data.path the document or collection path to delete.
 */
exports.recursiveDelete = functions
  .runWith({
    timeoutSeconds: 540,
    memory: '2GB'
  })
  .https.onCall(async (data, context) => {
    // Only allow admin users to execute this function.
    if (!(context.auth && context.auth.token && context.auth.token.admin)) {
      throw new functions.https.HttpsError(
        'permission-denied',
        'Must be an administrative user to initiate delete.'
      );
    }

    const path = data.path;
    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
    };
  });

クライアントからの呼び出し

関数を呼び出すには、Firebase SDK から関数への参照を取得し、必要なパラメータを渡します。

ウェブ
/**
 * Call the 'recursiveDelete' callable function with a path to initiate
 * a server-side delete.
 */
function deleteAtPath(path) {
    var deleteFn = firebase.functions().httpsCallable('recursiveDelete');
    deleteFn({ path: path })
        .then(function(result) {
            logMessage('Delete success: ' + JSON.stringify(result));
        })
        .catch(function(err) {
            logMessage('Delete failed, see console,');
            console.warn(err);
        });
}
Swift
    // Snippet not yet written
    
Objective-C
    // Snippet not yet written
    
Java
Android
/**
 * Call the 'recursiveDelete' callable function with a path to initiate
 * a server-side delete.
 */
public void deleteAtPath(String path) {
    Map<String, Object> data = new HashMap<>();
    data.put("path", path);

    HttpsCallableReference deleteFn =
            FirebaseFunctions.getInstance().getHttpsCallable("recursiveDelete");
    deleteFn.call(data)
            .addOnSuccessListener(new OnSuccessListener<HttpsCallableResult>() {
                @Override
                public void onSuccess(HttpsCallableResult httpsCallableResult) {
                    // Delete Success
                    // ...
                }
            })
            .addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    // Delete failed
                    // ...
                }
            });
}
Kotlin+KTX
Android
/**
 * Call the 'recursiveDelete' callable function with a path to initiate
 * a server-side delete.
 */
fun deleteAtPath(path: String) {
    val deleteFn = Firebase.functions.getHttpsCallable("recursiveDelete")
    deleteFn.call(hashMapOf("path" to path))
            .addOnSuccessListener {
                // Delete Success
                // ...
            }
            .addOnFailureListener {
                // Delete Failed
                // ...
            }
}

呼び出し可能な Cloud Functions 関数向けのクライアント SDK を使用すると、ユーザーの認証状態と path パラメータがリモート関数にシームレスに渡されます。関数が完了すると、クライアントは結果または例外が含まれるコールバックを受け取ります。Android、iOS、または別のプラットフォームから Cloud Functions 関数を呼び出す方法については、こちらのドキュメントをご覧ください。

制限事項

上記の解決策では、呼び出し可能関数からコレクションを削除していますが、次の制限事項がありますのでご注意ください。

  • 整合性 - 上記のコードは、一度に複数のドキュメントを削除します。削除オペレーションの進行中にクエリを行うと、クエリ結果では、部分的に完了した状態、つまり、ターゲットとなるドキュメントの一部のみが削除された状態が反映される場合があります。削除オペレーションがいつも同じように成功または失敗するという保証もないため、部分的な削除が発生したケースを処理する準備が必要です。
  • タイムアウト - 上記の関数は、タイムアウトまでの最大実行時間が 540 秒に構成されています。削除コードは、最適な場合に毎秒 4,000 のドキュメントを削除できます。2,000,000 を超えるドキュメントを削除する必要がある場合は、オペレーションがタイムアウトしないように、独自のサーバーでオペレーションを実行することを検討してください。独自のサーバーからコレクションを削除する方法の例については、コレクションを削除するをご覧ください。