Search code examples
flutterfirebasegoogle-cloud-platformgoogle-cloud-firestoretransactions

How to delete a document from a collection and create documents in another collection all that in a single transaction with Firestore in Flutter


To delete classified ad in my app I've got a two-step process which consists in:

  1. creating a new document in a collection while updating another one;
  2. updating the ad by updating with "deleted":true field.

I'd like it to be part of the same transaction to be atomic.

I am aware that a transaction could run multiple times depenging on the get which must all be at the beginning: and this puzzles me, as I wouldn't have multiple copies of the newly created documents.

Therefore I would appreciate your help in showing me how to do it?

My existing code is as follows:

// Deletes the ad or throw an error if it does not exist or if an error occured
  Future<void> transactionStoreDelete() async {
    final fs = FirebaseFirestore.instance;

    // == Move assets to to-be-deleted collections for future actual removal
    // from our servers and services
    // Note that if we do not succeed at moving the video assets to the
    // collection, there would be a yet-to-be-delvelopped server worker that
    // find unreferenced video and stream assets that will remove them from the
    // cloud and our services.
    try {
      await storeMarkVideoAndStreamToBeDeleted();

      await FirebaseFirestore.instance.runTransaction((t) async {
        /// Reads the ad document
        final doc = await t.get(fs.collection("ads").doc(id));

        if (doc.exists) {
          t.delete(doc.reference);
        }
      });
    } catch (e) {
      debugPrint(
          "Could not move video assets to to-be-deleted collections: $e");
    }
  }

With storeMarkVideoAndStreamToBeDeleted being as follows:

/// Clean up video assets byt doing the following:
  /// 1) Move the video and video stream to the to-be-deleted related collections;
  /// 2) Nullify [videoUrl] and [stream] in the current ad instance;
  /// 3) Update the ad in the collection.
  ///
  Future<void> storeMarkVideoAndStreamToBeDeleted() async {
    if (videoUrl != null) {
      try {
        // == Create record set in to be delted video
        await FirebaseFirestore.instance.collection("toBeDeletedVideos").add({
          ...trackingInformation,
          "url": videoUrl!,
        });
      } catch (e) {
        debugPrint("Could not move the video to be deleted: $e");
      }
    }

    if (stream != null) {
      try {
        // == Create record set in  to be deleted stream
        await FirebaseFirestore.instance.collection("toBeDeletedStreams").add({
          ...trackingInformation,
          "streamId": stream!.id,
        });
      } catch (e) {
        debugPrint("Could not move the stream video to be deleted: $e");
      }
    }

    // == Now erase reference to them
    return FirebaseFirestore.instance
        .collection("ads")
        .doc(id)
        .update({
      "videoUrl": null,
      "stream": null,
    });
  }

[UPDATE] Try to clarify the question after reading the comments.

[UPDATE 2] Wrote the name of the collections for clarification.

[UPDATE 3] Workflow in pseudo code of what I'd like to achieve:

t = openTheTransaction()

ad = 
adsCollection.getTheAdDocumentReferenceFrom(t)

t.create(toBeDeletedVideosCollection.createDocumentWith(ad))

t.create(toBeDeletedStreamsCollection.createDocumentWith(ad))

t.update(ad, {deleted:true})

t.commit()

Hope it helps.


Solution

  • UPDATE FOLLOWING UPDATES TO YOUR QUESTION

    You can implement the pseudo code in your question within a Transaction as follows:

    id = ...;
    db = FirebaseFirestore.instance;
    await db.runTransaction((t) async {
            
        final adDocRef = db.collection("ads").doc(id);
        final adDocSnapshot = await t.get(adDocRef);
    
        final trackingInformation = ...; /// I understand that you get trackingInformation from the adDocSnapshot
    
        final toBeDeletedVideoDocRef = db.collection("toBeDeletedVideos").doc();
        t.set(
          toBeDeletedVideoDocRef,
          {
            ...trackingInformation,
            "url": videoUrl!,
          } 
        );
    
        final toBeDeletedStreamDocRef = db.collection("toBeDeletedStreams").doc();
        t.set(
          toBeDeletedStreamDocRef,
          {
            ...trackingInformation,
            "streamId": stream!.id,
          } 
        );
    
        t.update(adDocRef, {"deleted":true});
    
    });
    

    Note that we use the set() method of a Transaction as well as the doc() method (of a CollectionReference) without providing any path in order to get an auto-generated docID.


    INITIAL ANSWER

    To delete classified ad in my app I've got a two-step process which consists in:

    • Creating a new document in a collection while updating another one;
    • Updating the ad by updating with "deleted":true field.

    I'd like it to be part of the same transaction to be atomic.

    If I correctly understand your question you want the 3 above actions (i.e. create a new doc and updating two docs) to be completed atomically.

    And this does not concern the operations in the storeMarkVideoAndStreamToBeDeleted() function, which is called before the above process.

    In this case you don't need to use a Transaction since you do not read any documents in your operation set, but you need to use a Batched Write which "completes atomically and can write (i.e. create, update & delete) to multiple documents".