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

Posted

技术标签:

【中文标题】在 Firestore 中删除包含所有子集合和嵌套子集合的文档【英文标题】:Delete a Document with all Subcollections and Nested Subcollections in Firestore 【发布时间】:2018-08-23 12:21:44 【问题描述】:

如何删除包含所有集合和嵌套子集合的 Document? (在函数环境中)

您可以在 RTDB 中 ref.child('../someNode).setValue(null) 并完成所需的行为。

我可以想到两种方法可以实现所需的删除行为,但都有非常可怕的缺点。

    创建一个“超级”函数,它将抓取每个文档并批量删除它们。 这个函数会很复杂,很容易改变,并且可能需要很长的执行时间。

    为每个文档类型添加“onDelete”触发器,并使其删除任何直接子集合。您将对根文档调用删除,删除调用将沿“树”传播。由于函数执行的巨大负载,这是缓慢的,可扩展的并且代价高昂。

假设您必须删除一个“GROUP”及其所有子项。使用 #1 会非常混乱,使用 #2 会很昂贵(每个文档调用 1 个函数)

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

是否有一种更好/更受欢迎/更简洁的方式来删除文档及其所有嵌套的子集合?

考虑到您可以从控制台执行此操作,这应该是可能的。

【问题讨论】:

什么触发了云函数调用? 用户将删除请求对象发布到具有调用“删除过程”的触发器的 RTDB。但实际上如何调用并不重要,这更多是关于如何处理实际删除树 【参考方案1】:

根据 Firebase 文档:https://firebase.google.com/docs/firestore/solutions/delete-collections 使用服务器端的 node-JS 可以轻松而整洁地删除带有嵌套子集合的集合。

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

【讨论】:

后人注意:如果您还没有,请在 package.json 中添加 "firebase-tools": "^8.0.1" 警告:包括 FireBASE 工具将滞后 ALOOOOOOTTTTTT!!! Typescript 用户注意:没有@types/firebase-tools 定义,但您可以像这样使用requireconst firebaseTools = require('firebase-tools'); 有没有办法在 firebase 模拟器中进行测试? 请注意 env.GCLOUD_PROJECT 已弃用,请改用 env.GCP_PROJECT。 cloud.google.com/functions/docs/…【参考方案2】:

不幸的是,您的分析是正确的,而且这个用例确实需要很多仪式。根据官方文档,firestore 中的单个 shot 不支持深度删除,无论是通过客户端库还是通过 rest-api 或 cli 工具。

cli 是开源的,它的实现在这里:https://github.com/firebase/firebase-tools/blob/master/src/firestore/delete.js。他们基本上实现了选项1。您在问题中描述了,因此您可以从那里获得一些灵感。

选项 1. 和 2. 都远非理想情况,为了使您的解决方案 100% 可靠,您需要保持一个 持久 队列与删除任务,因为长期运行过程中的任何错误将使您的系统处于某种不明确的状态。

我不鼓励使用原始选项 2。因为递归云函数调用可能很容易出错 - 例如,达到最大值。限制。

如果链接更改,请在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;

【讨论】:

我认为没有简单的方法可以让 CLI 删除命令在函数中工作。不过感谢您提供的信息,我会尝试将其移植到函数中 链接已失效 :-( @JimmyKane 谢谢我更新了答案并包含了代码。不幸的是,它似乎仍然不是执行原子删除的选项。 @arturgrzesiak 是的,这有点糟糕。我看到他们也有它的功能,开始遵循示例,在源代码中看到它也不是原子的。【参考方案3】:

我不知道对你有多大帮助,但测试它并比较我从 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());
      
    

【讨论】:

这对单个子集合很有用,但不会对嵌套的子集合做任何事情。【参考方案4】:

如上所述,您需要为此编写大量代码。对于每个要删除的文档,您需要检查它是否有一个或多个集合。如果是这样,那么您也需要将它们排队等待删除。我写了下面的代码来做到这一点。它未经测试可扩展到大型数据集,这对我来说很好,因为我在小规模集成测试后使用它进行清理。如果您需要更具可扩展性的东西,请随时以此为起点,尝试更多的批处理。

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);
  

【讨论】:

collections 在构造函数中从不使用。你的意思是写this._pendingCollections = collections || []; 吗? 为了让这个对我有用,我不得不将 getCollections 替换为 listCollections【参考方案5】:

使用 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)))
    


【讨论】:

不错,但批量删除会更好【参考方案6】:

对于那些不想或不能使用云功能的人,我在 admin sdk 中找到了一个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);

【讨论】:

【参考方案7】:
// 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];
  

【讨论】:

【参考方案8】:

您可以编写一个处理程序,在触发 onDelete Firestore 事件时递归删除所有嵌套的后代。

处理程序示例:

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);
;

将其与 firestore 事件联系起来:

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

【讨论】:

【参考方案9】:

将 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();
  ),
);

【讨论】:

【参考方案10】:

不确定这是否对这里的任何人有帮助,但我在使用 firebase-tools.firestore.delete 方法(firebase -工具版本 9.22.0)。

我目前正在使用返回的错误消息处理这些删除失败,以避免重写 Oleg Bondarenko 的回答中引用的代码。它使用 admin.firestore 来有效地删除失败的文档。

这是一个糟糕的解决方案,因为它依赖于错误消息,但至少它不会强迫我们复制整个 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(/\(default\)\/documents/) + 19))
          );

          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));
        
      
    );

【讨论】:

【参考方案11】:

您可以致电firebase.firestore().doc("whatever").set(),这将删除该文档中的所有内容。

.set 不会删除所有内容的唯一方法是将merge 标志设置为true

见Firestore Documentation on Add Data

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

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

【讨论】:

.set 不存在于集合中,仅存在于文档中。 不幸的是它不会替换此路径下的集合。

以上是关于在 Firestore 中删除包含所有子集合和嵌套子集合的文档的主要内容,如果未能解决你的问题,请参考以下文章

如何从 Firestore 中删除集合或子集合?

Firestore - 访问安全规则中的事务/嵌套数据

Cloud Firestore 如何查找包含特定文档 ID 的子集合的所有文档?

访问 Firestore 规则中的父文档字段

从Firebase的Cloud Firestore删除文档是否会删除该文档中的所有子集合?

来自多个引用的 Firestore 连接数据 [重复]