简体   繁体   English

在 Firestore 中删除包含所有子集合和嵌套子集合的文档

[英]Delete a Document with all Subcollections and Nested Subcollections in Firestore

How can you delete a Document with all it's collections and nested subcollections?如何删除包含所有 collections 和嵌套子集合的文档? (inside the functions environment) (在功能环境内)

In the RTDB you can ref.child('../someNode).setValue(null) and that completes the desired behavior.在 RTDB 中,您可以ref.child('../someNode).setValue(null)并完成所需的行为。

I can think of two ways you could achieve the desired delete behavior, both with tremendously ghastly drawbacks.我可以想到两种实现所需删除行为的方法,这两种方法都有非常可怕的缺点。

  1. Create a 'Super' function that will spider every document and delete them in a batch.创建一个“超级”function,它将抓取每个文档并批量删除它们。 This function would be complicated, brittle to changes, and might take a lengthy execution time.这个 function 会很复杂,易受更改,并且可能需要很长的执行时间。

  2. Add 'onDelete' triggers for each Document type, and make it delete any direct subcollections.为每种文档类型添加“onDelete”触发器,并使其删除任何直接子集合。 You'll call delete on the root document, and the deletion calls will propagate down the 'tree'.您将对根文档调用 delete,删除调用将沿着“树”传播。 This is sluggish, scales atrociously and is costly due to the colossal load of function executions.由于 function 次执行的巨大负载,这很慢,扩展很糟糕并且代价高昂。

Imagine you would have to delete a 'GROUP' and all it's children.想象一下,您必须删除一个“GROUP”及其所有子项。 It would be deeply chaotic with #1 and pricey with #2 (1 function call per doc) #1 会非常混乱,#2 会很昂贵(每个文档 1 function 次调用)

groups > GROUP > projects > PROJECT > files > FILE > assets > ASSET
                                                   > urls > URL
                                    > members > MEMBER
               > questions > QUESTION > answers > ANSWER > replies > REPLY
                                      > comments > COMMENT
               > resources > RESOURCE > submissions > SUBMISSION
                                      > requests > REQUEST

Is there a superior/favored/cleaner way to delete a document and all it's nested subcollections?是否有更好/更受欢迎/更清洁的方法来删除文档及其所有嵌套子集合?

It ought to be possible considering you can do it from the console.考虑到您可以从控制台执行此操作,这应该是可能的。

according to firebase documentation:根据firebase文档:
https://firebase.google.com/docs/firestore/solutions/delete-collections https://firebase.google.com/docs/firestore/solutions/delete-collections
Deleting collection with nested subcollections might be done easy and neat with node-JS on the server side.删除带有嵌套子集合的集合可能会在服务器端使用 node-JS 轻松而整洁地完成。

const client = require('firebase-tools');
await client.firestore
      .delete(collectionPath, {
        project: process.env.GCLOUD_PROJECT,
        recursive: true,
        yes: true
      }); 

Unfortunately, your analysis is spot on and indeed this use case does require a lot of ceremony.不幸的是,你的分析是正确的,而且这个用例确实需要很多仪式。 According to official documentation, there is no support for deep deletes in a single shot in firestore neither via client libraries nor rest-api nor the cli tool.根据官方文档,对于在一个单深删除不支持firestore既不通过客户端库,也没有REST的API ,也不是cli工具。

The cli is open sourced and its implementation lives here: https://github.com/firebase/firebase-tools/blob/master/src/firestore/delete.js . cli是开源的,其实现位于: https : //github.com/firebase/firebase-tools/blob/master/src/firestore/delete.js They basically implemented option 1. you described in your question, so you can take some inspiration from there.他们基本上实现了选项 1。你在你的问题中描述过,所以你可以从那里得到一些灵感。

Both options 1. and 2. are far from ideal situation and to make your solution 100% reliable you will need to keep a persistent queue with deletion tasks, as any error in the long running procedure will leave your system in some ill-defined state.选项 1. 和 2. 都远非理想情况,为了使您的解决方案 100% 可靠,您需要保留一个包含删除任务的持久队列,因为长时间运行的过程中的任何错误都会使您的系统处于某种不明确的状态.

I would discourage to go with raw option 2. as recursive cloud function calls may very easily went wrong - for example, hitting max.我不鼓励使用原始选项 2。因为递归云函数调用可能很容易出错 - 例如,达到最大值。 limits.限制。

In case the link changed, below the full source of https://github.com/firebase/firebase-tools/blob/master/src/firestore/delete.js :如果链接发生更改,请在https://github.com/firebase/firebase-tools/blob/master/src/firestore/delete.js的完整来源下方:

"use strict";

var clc = require("cli-color");
var ProgressBar = require("progress");

var api = require("../api");
var firestore = require("../gcp/firestore");
var FirebaseError = require("../error");
var logger = require("../logger");
var utils = require("../utils");

/**
 * Construct a new Firestore delete operation.
 *
 * @constructor
 * @param {string} project the Firestore project ID.
 * @param {string} path path to a document or collection.
 * @param {boolean} options.recursive true if the delete should be recursive.
 * @param {boolean} options.shallow true if the delete should be shallow (non-recursive).
 * @param {boolean} options.allCollections true if the delete should universally remove all collections and docs.
 */
function FirestoreDelete(project, path, options) {
  this.project = project;
  this.path = path;
  this.recursive = Boolean(options.recursive);
  this.shallow = Boolean(options.shallow);
  this.allCollections = Boolean(options.allCollections);

  // Remove any leading or trailing slashes from the path
  if (this.path) {
    this.path = this.path.replace(/(^\/+|\/+$)/g, "");
  }

  this.isDocumentPath = this._isDocumentPath(this.path);
  this.isCollectionPath = this._isCollectionPath(this.path);

  this.allDescendants = this.recursive;
  this.parent = "projects/" + project + "/databases/(default)/documents";

  // When --all-collections is passed any other flags or arguments are ignored
  if (!options.allCollections) {
    this._validateOptions();
  }
}

/**
 * Validate all options, throwing an exception for any fatal errors.
 */
FirestoreDelete.prototype._validateOptions = function() {
  if (this.recursive && this.shallow) {
    throw new FirebaseError("Cannot pass recursive and shallow options together.");
  }

  if (this.isCollectionPath && !this.recursive && !this.shallow) {
    throw new FirebaseError("Must pass recursive or shallow option when deleting a collection.");
  }

  var pieces = this.path.split("/");

  if (pieces.length === 0) {
    throw new FirebaseError("Path length must be greater than zero.");
  }

  var hasEmptySegment = pieces.some(function(piece) {
    return piece.length === 0;
  });

  if (hasEmptySegment) {
    throw new FirebaseError("Path must not have any empty segments.");
  }
};

/**
 * Determine if a path points to a document.
 *
 * @param {string} path a path to a Firestore document or collection.
 * @return {boolean} true if the path points to a document, false
 * if it points to a collection.
 */
FirestoreDelete.prototype._isDocumentPath = function(path) {
  if (!path) {
    return false;
  }

  var pieces = path.split("/");
  return pieces.length % 2 === 0;
};

/**
 * Determine if a path points to a collection.
 *
 * @param {string} path a path to a Firestore document or collection.
 * @return {boolean} true if the path points to a collection, false
 * if it points to a document.
 */
FirestoreDelete.prototype._isCollectionPath = function(path) {
  if (!path) {
    return false;
  }

  return !this._isDocumentPath(path);
};

/**
 * Construct a StructuredQuery to find descendant documents of a collection.
 *
 * See:
 * https://firebase.google.com/docs/firestore/reference/rest/v1beta1/StructuredQuery
 *
 * @param {boolean} allDescendants true if subcollections should be included.
 * @param {number} batchSize maximum number of documents to target (limit).
 * @param {string=} startAfter document name to start after (optional).
 * @return {object} a StructuredQuery.
 */
FirestoreDelete.prototype._collectionDescendantsQuery = function(
  allDescendants,
  batchSize,
  startAfter
) {
  var nullChar = String.fromCharCode(0);

  var startAt = this.parent + "/" + this.path + "/" + nullChar;
  var endAt = this.parent + "/" + this.path + nullChar + "/" + nullChar;

  var where = {
    compositeFilter: {
      op: "AND",
      filters: [
        {
          fieldFilter: {
            field: {
              fieldPath: "__name__",
            },
            op: "GREATER_THAN_OR_EQUAL",
            value: {
              referenceValue: startAt,
            },
          },
        },
        {
          fieldFilter: {
            field: {
              fieldPath: "__name__",
            },
            op: "LESS_THAN",
            value: {
              referenceValue: endAt,
            },
          },
        },
      ],
    },
  };

  var query = {
    structuredQuery: {
      where: where,
      limit: batchSize,
      from: [
        {
          allDescendants: allDescendants,
        },
      ],
      select: {
        fields: [{ fieldPath: "__name__" }],
      },
      orderBy: [{ field: { fieldPath: "__name__" } }],
    },
  };

  if (startAfter) {
    query.structuredQuery.startAt = {
      values: [{ referenceValue: startAfter }],
      before: false,
    };
  }

  return query;
};

/**
 * Construct a StructuredQuery to find descendant documents of a document.
 * The document itself will not be included
 * among the results.
 *
 * See:
 * https://firebase.google.com/docs/firestore/reference/rest/v1beta1/StructuredQuery
 *
 * @param {boolean} allDescendants true if subcollections should be included.
 * @param {number} batchSize maximum number of documents to target (limit).
 * @param {string=} startAfter document name to start after (optional).
 * @return {object} a StructuredQuery.
 */
FirestoreDelete.prototype._docDescendantsQuery = function(allDescendants, batchSize, startAfter) {
  var query = {
    structuredQuery: {
      limit: batchSize,
      from: [
        {
          allDescendants: allDescendants,
        },
      ],
      select: {
        fields: [{ fieldPath: "__name__" }],
      },
      orderBy: [{ field: { fieldPath: "__name__" } }],
    },
  };

  if (startAfter) {
    query.structuredQuery.startAt = {
      values: [{ referenceValue: startAfter }],
      before: false,
    };
  }

  return query;
};

/**
 * Query for a batch of 'descendants' of a given path.
 *
 * For document format see:
 * https://firebase.google.com/docs/firestore/reference/rest/v1beta1/Document
 *
 * @param {boolean} allDescendants true if subcollections should be included,
 * @param {number} batchSize the maximum size of the batch.
 * @param {string=} startAfter the name of the document to start after (optional).
 * @return {Promise<object[]>} a promise for an array of documents.
 */
FirestoreDelete.prototype._getDescendantBatch = function(allDescendants, batchSize, startAfter) {
  var url;
  var body;
  if (this.isDocumentPath) {
    url = this.parent + "/" + this.path + ":runQuery";
    body = this._docDescendantsQuery(allDescendants, batchSize, startAfter);
  } else {
    url = this.parent + ":runQuery";
    body = this._collectionDescendantsQuery(allDescendants, batchSize, startAfter);
  }

  return api
    .request("POST", "/v1beta1/" + url, {
      auth: true,
      data: body,
      origin: api.firestoreOrigin,
    })
    .then(function(res) {
      // Return the 'document' property for each element in the response,
      // where it exists.
      return res.body
        .filter(function(x) {
          return x.document;
        })
        .map(function(x) {
          return x.document;
        });
    });
};

/**
 * Progress bar shared by the class.
 */
FirestoreDelete.progressBar = new ProgressBar("Deleted :current docs (:rate docs/s)", {
  total: Number.MAX_SAFE_INTEGER,
});

/**
 * Repeatedly query for descendants of a path and delete them in batches
 * until no documents remain.
 *
 * @return {Promise} a promise for the entire operation.
 */
FirestoreDelete.prototype._recursiveBatchDelete = function() {
  var self = this;

  // Tunable deletion parameters
  var readBatchSize = 7500;
  var deleteBatchSize = 250;
  var maxPendingDeletes = 15;
  var maxQueueSize = deleteBatchSize * maxPendingDeletes * 2;

  // All temporary variables for the deletion queue.
  var queue = [];
  var numPendingDeletes = 0;
  var pagesRemaining = true;
  var pageIncoming = false;
  var lastDocName;

  var failures = [];
  var retried = {};

  var queueLoop = function() {
    if (queue.length == 0 && numPendingDeletes == 0 && !pagesRemaining) {
      return true;
    }

    if (failures.length > 0) {
      logger.debug("Found " + failures.length + " failed deletes, failing.");
      return true;
    }

    if (queue.length <= maxQueueSize && pagesRemaining && !pageIncoming) {
      pageIncoming = true;

      self
        ._getDescendantBatch(self.allDescendants, readBatchSize, lastDocName)
        .then(function(docs) {
          pageIncoming = false;

          if (docs.length == 0) {
            pagesRemaining = false;
            return;
          }

          queue = queue.concat(docs);
          lastDocName = docs[docs.length - 1].name;
        })
        .catch(function(e) {
          logger.debug("Failed to fetch page after " + lastDocName, e);
          pageIncoming = false;
        });
    }

    if (numPendingDeletes > maxPendingDeletes) {
      return false;
    }

    if (queue.length == 0) {
      return false;
    }

    var toDelete = [];
    var numToDelete = Math.min(deleteBatchSize, queue.length);

    for (var i = 0; i < numToDelete; i++) {
      toDelete.push(queue.shift());
    }

    numPendingDeletes++;
    firestore
      .deleteDocuments(self.project, toDelete)
      .then(function(numDeleted) {
        FirestoreDelete.progressBar.tick(numDeleted);
        numPendingDeletes--;
      })
      .catch(function(e) {
        // For server errors, retry if the document has not yet been retried.
        if (e.status >= 500 && e.status < 600) {
          logger.debug("Server error deleting doc batch", e);

          // Retry each doc up to one time
          toDelete.forEach(function(doc) {
            if (retried[doc.name]) {
              logger.debug("Failed to delete doc " + doc.name + " multiple times.");
              failures.push(doc.name);
            } else {
              retried[doc.name] = true;
              queue.push(doc);
            }
          });
        } else {
          logger.debug("Fatal error deleting docs ", e);
          failures = failures.concat(toDelete);
        }

        numPendingDeletes--;
      });

    return false;
  };

  return new Promise(function(resolve, reject) {
    var intervalId = setInterval(function() {
      if (queueLoop()) {
        clearInterval(intervalId);

        if (failures.length == 0) {
          resolve();
        } else {
          reject("Failed to delete documents " + failures);
        }
      }
    }, 0);
  });
};

/**
 * Delete everything under a given path. If the path represents
 * a document the document is deleted and then all descendants
 * are deleted.
 *
 * @return {Promise} a promise for the entire operation.
 */
FirestoreDelete.prototype._deletePath = function() {
  var self = this;
  var initialDelete;
  if (this.isDocumentPath) {
    var doc = { name: this.parent + "/" + this.path };
    initialDelete = firestore.deleteDocument(doc).catch(function(err) {
      logger.debug("deletePath:initialDelete:error", err);
      if (self.allDescendants) {
        // On a recursive delete, we are insensitive to
        // failures of the initial delete
        return Promise.resolve();
      }

      // For a shallow delete, this error is fatal.
      return utils.reject("Unable to delete " + clc.cyan(this.path));
    });
  } else {
    initialDelete = Promise.resolve();
  }

  return initialDelete.then(function() {
    return self._recursiveBatchDelete();
  });
};

/**
 * Delete an entire database by finding and deleting each collection.
 *
 * @return {Promise} a promise for all of the operations combined.
 */
FirestoreDelete.prototype.deleteDatabase = function() {
  var self = this;
  return firestore
    .listCollectionIds(this.project)
    .catch(function(err) {
      logger.debug("deleteDatabase:listCollectionIds:error", err);
      return utils.reject("Unable to list collection IDs");
    })
    .then(function(collectionIds) {
      var promises = [];

      logger.info("Deleting the following collections: " + clc.cyan(collectionIds.join(", ")));

      for (var i = 0; i < collectionIds.length; i++) {
        var collectionId = collectionIds[i];
        var deleteOp = new FirestoreDelete(self.project, collectionId, {
          recursive: true,
        });

        promises.push(deleteOp.execute());
      }

      return Promise.all(promises);
    });
};

/**
 * Check if a path has any children. Useful for determining
 * if deleting a path will affect more than one document.
 *
 * @return {Promise<boolean>} a promise that retruns true if the path has
 * children and false otherwise.
 */
FirestoreDelete.prototype.checkHasChildren = function() {
  return this._getDescendantBatch(true, 1).then(function(docs) {
    return docs.length > 0;
  });
};

/**
 * Run the delete operation.
 */
FirestoreDelete.prototype.execute = function() {
  var verifyRecurseSafe;
  if (this.isDocumentPath && !this.recursive && !this.shallow) {
    verifyRecurseSafe = this.checkHasChildren().then(function(multiple) {
      if (multiple) {
        return utils.reject("Document has children, must specify -r or --shallow.", { exit: 1 });
      }
    });
  } else {
    verifyRecurseSafe = Promise.resolve();
  }

  var self = this;
  return verifyRecurseSafe.then(function() {
    return self._deletePath();
  });
};

module.exports = FirestoreDelete;

i don't know how much helpful for you but test it and compare the execution time which i get use it from fire store doc我不知道对您有多大帮助,但对其进行测试并比较我从 fire store doc 中使用它的执行时间

 /** Delete a collection in batches to avoid out-of-memory errors. * Batch size may be tuned based on document size (atmost 1MB) and application requirements.
    */



 void deleteCollection(CollectionReference collection, int batchSize) {
      try {
        // retrieve a small batch of documents to avoid out-of-memory errors
        ApiFuture<QuerySnapshot> future = collection.limit(batchSize).get();
        int deleted = 0;
        // future.get() blocks on document retrieval
        List<QueryDocumentSnapshot> documents = future.get().getDocuments();
        for (QueryDocumentSnapshot document : documents) {
          document.getReference().delete();
          ++deleted;
        }
        if (deleted >= batchSize) {
          // retrieve and delete another batch
          deleteCollection(collection, batchSize);
        }
      } catch (Exception e) {
        System.err.println("Error deleting collection : " + e.getMessage());
      }
    }

As mentioned above, you need to write good bit of code for this.如上所述,您需要为此编写大量代码。 For each document that is to be deleted you need to check if it has one or more collections.对于要删除的每个文档,您需要检查它是否有一个或多个集合。 If it does, then you need to queue those up for deletion too.如果是这样,那么您也需要将它们排入队列以进行删除。 I wrote the code below to do this.我写了下面的代码来做到这一点。 It's not tested to be scalable to large data sets, which is fine for me as I'm using it to clean up after small scale integration tests.它没有经过测试可以扩展到大型数据集,这对我来说很好,因为我在小规模集成测试后使用它进行清理。 If you need something more scalable, feel free to take this as a starting point and play around with batching more.如果您需要更具可扩展性的东西,请随意将其作为起点并更多地进行批处理。

class FirebaseDeleter {


constructor(database, collections) {
    this._database = database;
    this._pendingCollections = [];
  }

  run() {
    return new Promise((resolve, reject) => {
      this._callback = resolve;
      this._database.getCollections().then(collections => {
        this._pendingCollections = collections;
        this._processNext();
      });
    });
  }

  _processNext() {
    const collections = this._pendingCollections;
    this._pendingCollections = [];
    const promises = collections.map(collection => {
      return this.deleteCollection(collection, 10000);
    });

    Promise.all(promises).then(() => {
      if (this._pendingCollections.length == 0) {
        this._callback();
      } else {
        process.nextTick(() => {
          this._processNext();
        });
      }
    });
  }

  deleteCollection(collectionRef, batchSize) {
    var query = collectionRef;

    return new Promise((resolve, reject) => {
      this.deleteQueryBatch(query, batchSize, resolve, reject);
    });
  }

  deleteQueryBatch(query, batchSize, resolve, reject) {
    query
      .get()
      .then(snapshot => {
        // When there are no documents left, we are done
        if (snapshot.size == 0) {
          return 0;
        }

        // Delete documents in a batch
        var batch = this._database.batch();
        const collectionPromises = [];
        snapshot.docs.forEach(doc => {
          collectionPromises.push(
            doc.ref.getCollections().then(collections => {
              collections.forEach(collection => {
                this._pendingCollections.push(collection);
              });
            })
          );
          batch.delete(doc.ref);
        });

        // Wait until we know if all the documents have collections before deleting them.
        return Promise.all(collectionPromises).then(() => {
          return batch.commit().then(() => {
            return snapshot.size;
          });
        });
      })
      .then(numDeleted => {
        if (numDeleted === 0) {
          resolve();
          return;
        }

        // Recurse on the next process tick, to avoid
        // exploding the stack.
        process.nextTick(() => {
          this.deleteQueryBatch(query, batchSize, resolve, reject);
        });
      })
      .catch(reject);
  }
}

Solution using Node.js Admin SDK使用 Node.js Admin SDK 的解决方案


export const deleteDocument = async (doc: FirebaseFirestore.DocumentReference) => {
    const collections = await doc.listCollections()
    await Promise.all(collections.map(collection => deleteCollection(collection)))
    await doc.delete()
}

export const deleteCollection = async (collection: FirebaseFirestore.CollectionReference) => {
    const query = collection.limit(100)
    while (true) {
        const snap = await query.get()
        if (snap.empty) {
            return
        }
        await Promise.all(snap.docs.map(doc => deleteDocument(doc.ref)))
    }
}

There is now a simple way delete a document and all of its subcollections using NodeJS.现在有一种使用 NodeJS 删除文档及其所有子集合的简单方法。

This was made available in nodejs-firestore version v4.11.0.这在 nodejs-firestore 版本 v4.11.0 中可用。

From the docs: recursiveDelete() Recursively deletes all documents and subcollections at and under the specified level.来自文档: recursiveDelete()递归删除指定级别和指定级别下的所有文档和子集合。

import * as admin from 'firebase-admin'

const ref = admin.firestore().doc('my_document')
admin.firestore().recursiveDelete(ref)
// You can add all the collection hierarchy to object
private collectionsHierarchy = { 

    groups: [

        [
        'groups',
        'projects', 
        'files', 
        'assets',
        'urls',
        'members'
      ]

    ]

 };

async deleteDocument(rootDocument: string) {
     // if (!rootDocument.startsWith(`groups/${this.groupId()}`)) {
     //  rootDocument = `groups/${this.groupId()}/${rootDocument}`;
     // }

    const batchSize: number = 100;
    let root = await this.db
      .doc(rootDocument)
      .get()
      .toPromise();

    if (!root.exists) {
      return;
    }

    const segments = rootDocument.split('/');
    const documentCollection = segments[segments.length - 2]; 
    const allHierarchies = this.collectionsHierarchy[documentCollection];

    for (let i = 0; i < allHierarchies.length; i = i + 1) {
      const hierarchy = allHierarchies[i];
      const collectionIndex = hierarchy.indexOf(documentCollection) + 1;
      const nextCollections: [] = hierarchy.slice(collectionIndex);

      const stack = [`${root.ref.path}/${nextCollections.shift()}`];

      while (stack.length) {
        const path = stack.pop();
        const collectionRef = this.db.firestore.collection(path);
        const query = collectionRef.orderBy('__name__').limit(batchSize);
        let deletedIems = await this.deleteQueryBatch(query, batchSize);
        const nextCollection = nextCollections.shift();
        deletedIems = deletedIems.map(di => `${di}/${nextCollection}`);
        stack.push(...deletedIems);
      }
    }

    await root.ref.delete();
  }

  private async deleteQueryBatch(
    query: firebase.firestore.Query,
    batchSize: number
  ) {
    let deletedItems: string[] = [];
    let snapshot = await query.get();

    if (snapshot.size === 0) {
      return deletedItems;
    }

    const batch = this.db.firestore.batch();
    snapshot.docs.forEach(doc => {
      deletedItems.push(doc.ref.path);
      batch.delete(doc.ref);
    });


    await batch.commit();

    if (snapshot.size === 0) {
      return deletedItems;
    }

    const result = await this.deleteQueryBatch(query, batchSize);
    return [...deletedItems, ...result];
  }

You can write a handler which will recursive delete all nested descendants when triggers onDelete Firestore event.您可以编写一个处理程序,当触发 onDelete Firestore 事件时,它将递归删除所有嵌套的后代。

Example of handler:处理程序示例:

const deleteDocumentWithDescendants = async (documentSnap: FirebaseFirestore.QueryDocumentSnapshot) => {
  return documentSnap.ref.listCollections().then((subCollections) => {
    subCollections.forEach((subCollection) => {
      return subCollection.get().then((snap) => {
        snap.forEach((doc) => {
          doc.ref.delete();
          deleteDocumentWithDescendants(doc);
        });
      });
    });
  });
};

// On any document delete
export const onDocumentDelete = async (documentSnap: FirebaseFirestore.QueryDocumentSnapshot) => {
  await deleteDocumentWithDescendants(documentSnap);
};

Tie it up with firestore event:将其与 firestore 事件联系起来:

exports.onDeleteDocument = functions.firestore.document('{collectionId}/{docId}')
    .onDelete(onDocumentDelete);

Another solution using Node.js Admin SDK with Batch.另一个使用 Node.js Admin SDK 和 Batch 的解决方案。

const traverseDocumentRecursively = async (
  docRef: FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData>,
  accumulatedRefs: 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) {
        accumulatedRefs.push(doc.ref); 
        await traverseDocumentRecursively(doc.ref, accumulatedRefs);
      } 
    } 
  } 
};
import { chunk } from 'lodash';

const doc = admin.firestore().collection('users').doc('001');
const accumulatedRefs: FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData>[] = [];

await traverseDocumentRecursively(doc, accumulatedRefs);
await Promise.all(
  // Each transaction or batch of writes can write to a maximum of 500 documents
  chunk(accumulatedRefs, 500).map((chunkedRefs) => {
    const batch = admin.firestore().batch();
    for (const ref of chunkedRefs) {
      batch.delete(ref);
    } 
    return batch.commit();
  }),
);

Not sure if this is helpful for anyone here, but I am frequently facing the error "Fatal error deleting docs <list of docs>" when using firebase-tools.firestore.delete method (firebase-tools version 9.22.0).不确定这对这里的任何人是否有帮助,但在使用firebase-tools.firestore.delete方法( firebase -tools 版本 9.22.0)时,我经常遇到错误“删除文档<文档列表>的致命错误”。

I am currently handling these deletion failures using the returned error message in order to avoid rewriting the code cited at Oleg Bondarenko's answer.我目前正在使用返回的错误消息处理这些删除失败,以避免重写 Oleg Bondarenko 的回答中引用的代码。 It uses admin.firestore to effectively delete the failed docs.它使用admin.firestore来有效地删除失败的文档。

It's a poor solution since it relies on the error message, but at least it doesn't force us to copy the whole FirestoreDelete code to modify just a few lines of it:这是一个糟糕的解决方案,因为它依赖于错误消息,但至少它不会强迫我们复制整个 FirestoreDelete 代码来修改其中的几行:

firebase_tools.firestore
    .delete(path, {
      project: JSON.parse(process.env.FIREBASE_CONFIG!).projectId,
      recursive: true,
      yes: true,
      token: getToken(),
    })
    .catch((err: Error) => {
      if (err.name == "FirebaseError") {
        // If recursive delete fails to delete some of the documents, 
        // parse the failures from the error message and delete it manually
        const failedDeletingDocs = err.message.match(
          /.*Fatal error deleting docs ([^\.]+)/
        );
        if (failedDeletingDocs) {
          const docs = failedDeletingDocs[1].split(", ");
          const docRefs = docs.map((doc) =>
            firestore.doc(doc.slice(doc.search(/\/companies/)))
          );

          firestore
            .runTransaction(async (t) => {
              docRefs.forEach((doc) => t.delete(doc));
              return docs;
            })
            .then((docs) =>
              console.log(
                "Succesfully deleted docs after failing: " + docs.join(", ")
              )
            )
            .catch((err) => console.error(err));
        }
      }
    });

For those who don't want or can't use cloud functions, I found a recursiveDelete function in the admin sdk:对于那些不想或不能使用云函数的人,我在 admin sdk 中找到了一个recursiveDelete函数:

https://googleapis.dev/nodejs/firestore/latest/Firestore.html#recursiveDelete https://googleapis.dev/nodejs/firestore/latest/Firestore.html#recursiveDelete

// Recursively delete a reference and log the references of failures.
const bulkWriter = firestore.bulkWriter();
bulkWriter
  .onWriteError((error) => {
    if (
      error.failedAttempts < MAX_RETRY_ATTEMPTS
    ) {
      return true;
    } else {
      console.log('Failed write at document: ', error.documentRef.path);
      return false;
    }
  });
await firestore.recursiveDelete(docRef, bulkWriter);

If you are looking to delete user data, a solution to consider in 2022 is the Delete User Data Firebase Extension.如果您要删除用户数据,2022 年要考虑的解决方案是删除用户数据Firebase 扩展。

Once this is active, you can simply delete the user from Firebase Auth to trigger the recursive deletion of the user documents:一旦激活,您只需从 Firebase Auth 中删除用户即可触发用户文档的递归删除:

import admin from "firebase-admin";
admin.auth().deleteUser(userId); 

You can call firebase.firestore().doc("whatever").set() and that will delete everything in that document.您可以调用firebase.firestore().doc("whatever").set()这将删除该文档中的所有内容。

The only way .set does not erase everything is if you set the merge flag to true . .set不会擦除所有内容的唯一方法是将merge标志设置为true

SeeFirestore Documentation on Add Data请参阅有关添加数据的 Firestore 文档

var cityRef = db.collection('cities').doc('BJ');

var setWithMerge = cityRef.set({
    capital: true
}, { merge: true });

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 删除 Firestore 文档是否会删除其子集合? - Does deleting a firestore document delete its subcollections? Firestore - 递归复制文档及其所有子集合/文档 - Firestore - Recursively Copy a Document and all it's subcollections/documents Firestore TTL 是否会删除子集合中的文档? - Does Firestore TTL delete documents in subcollections? 如何在 Cloud Firestore 文档中列出子集合 - How to list subcollections in a Cloud Firestore document Arrays 或 Firebase Firestore 中的子集合? - Arrays or subcollections in Firebase Firestore? Firebase Firestore 规则子集合 - Firebase Firestore Rules Subcollections 如何删除 firebase 和 flutter 和 dart 中所有子集合的文档 - How can I delete a document with all its subcollections in firebase with flutter and dart 如何使用 Android 在 RecyclerView 中显示来自 Firestore 的文档及其子集合? - How to display a document with its subcollections from Firestore in a RecyclerView with Android? Firestore 规则:使集合中的文档和所有子集合遵循相同的规则 - Firestore Rules: Making documents in a collection and all subcollections follow the same rule 如何在 Firestore 中递归列出子集合 - How to recursively list subcollections in Firestore
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM