You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中Firestore单事务实现文档删除与跨集合创建

问题描述

我在应用中删除分类广告原本采用两步流程:

  1. 在一个集合创建新文档,同时更新另一个集合;
  2. 为广告文档添加"deleted":true字段。

希望将这些操作纳入同一Firestore事务以实现原子性,但担心事务因开头的get操作多次重试,会重复创建新文档,寻求实现方案。


现有代码

transactionStoreDelete方法

// 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");
  }
}

storeMarkVideoAndStreamToBeDeleted方法

/// Clean up video assets by 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 deleted 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,
  });
}

期望的伪代码流程

t = openTheTransaction()

ad = adsCollection.getTheAdDocumentReferenceFrom(t)

t.create(toBeDeletedVideosCollection.createDocumentWith(ad))

t.create(toBeDeletedStreamsCollection.createDocumentWith(ad))

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

t.commit()

实现方案

核心解决思路是让事务内的创建操作具备幂等性,即多次执行结果与单次执行一致,避免重试时重复生成文档。以下提供两种可行方案:

方案一:基于广告ID生成固定删除记录ID

不再使用add()生成自动ID,而是用广告ID拼接固定后缀作为删除记录的文档ID。事务重试时,尝试创建已存在的文档会触发失败,Firestore会自动重试直到整个事务成功,最终只会生成一条删除记录。

修改后的事务代码:

Future<void> transactionStoreDelete() async {
  final fs = FirebaseFirestore.instance;
  final adRef = fs.collection("ads").doc(id);

  await FirebaseFirestore.instance.runTransaction((t) async {
    // 校验广告文档存在且未被标记删除
    final adDoc = await t.get(adRef);
    if (!adDoc.exists) {
      throw Exception("广告文档不存在");
    }
    final adData = adDoc.data()!;
    if (adData["deleted"] == true) {
      return; // 已完成删除流程,无需重复操作
    }

    // 创建视频删除记录(固定ID)
    if (adData["videoUrl"] != null) {
      final deleteVideoRef = fs.collection("toBeDeletedVideos").doc("ad-${id}-video");
      // 仅当文档不存在时创建,确保幂等
      await t.set(deleteVideoRef, {
        ...trackingInformation,
        "url": adData["videoUrl"],
      }, SetOptions(merge: false));
    }

    // 创建流删除记录(固定ID)
    if (adData["stream"] != null) {
      final deleteStreamRef = fs.collection("toBeDeletedStreams").doc("ad-${id}-stream");
      await t.set(deleteStreamRef, {
        ...trackingInformation,
        "streamId": adData["stream"]["id"],
      }, SetOptions(merge: false));
    }

    // 标记广告为已删除并清空资源引用
    await t.update(adRef, {
      "deleted": true,
      "videoUrl": null,
      "stream": null,
    });
  });
}

方案二:在广告文档中记录删除记录ID

在广告文档中新增字段记录已生成的删除记录ID,事务内先检查这些字段是否存在,不存在才执行创建操作。重试时因广告文档已更新,会跳过重复创建步骤。

示例代码:

Future<void> transactionStoreDelete() async {
  final fs = FirebaseFirestore.instance;
  final adRef = fs.collection("ads").doc(id);

  await FirebaseFirestore.instance.runTransaction((t) async {
    final adDoc = await t.get(adRef);
    if (!adDoc.exists) {
      throw Exception("广告文档不存在");
    }
    final adData = adDoc.data()!;
    if (adData["deleted"] == true) return;

    // 创建视频删除记录并记录ID到广告文档
    if (adData["videoUrl"] != null && adData["deletedVideoId"] == null) {
      final deleteVideoRef = fs.collection("toBeDeletedVideos").doc();
      await t.set(deleteVideoRef, {
        ...trackingInformation,
        "url": adData["videoUrl"],
      });
      await t.update(adRef, {"deletedVideoId": deleteVideoRef.id});
    }

    // 创建流删除记录并记录ID到广告文档
    if (adData["stream"] != null && adData["deletedStreamId"] == null) {
      final deleteStreamRef = fs.collection("toBeDeletedStreams").doc();
      await t.set(deleteStreamRef, {
        ...trackingInformation,
        "streamId": adData["stream"]["id"],
      });
      await t.update(adRef, {"deletedStreamId": deleteStreamRef.id});
    }

    // 标记广告为已删除并清空资源引用
    await t.update(adRef, {
      "deleted": true,
      "videoUrl": null,
      "stream": null,
    });
  });
}

内容的提问来源于stack exchange,提问作者Stéphane de Luca

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 04:46:37