Flutter中Firestore单事务实现文档删除与跨集合创建
问题描述
我在应用中删除分类广告原本采用两步流程:
- 在一个集合创建新文档,同时更新另一个集合;
- 为广告文档添加
"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
相关产品推荐
相关产品推荐

