Firestore监听软删除文档时修改事件被识别为新增事件问题
介绍
为了无需下载整个集合就能监听到集合中所有被删除的文档,我采用了社区提到的方案:
不要彻底删除文档,只给它新增一个
deleted: true属性,然后监听查询db.collection("cities").where("deleted", "==", true)即可。
问题描述
我遇到了无法解释的问题:Firestore监听器将所有"modified"事件都判定为"added"事件触发。
聊天室删除代码
我正在实现带分页的聊天列表页面,页面挂载时仅拉取历史文档,同时订阅页面挂载时间new Date()之后所有聊天室文档的变更。
我通过如下逻辑"删除"聊天室文档:
async function deleteChatRoom(chatRoomId, userId) { const chatRoomRef = firestore.collection("chats").doc(chatRoomId); const chatRoomDoc = await chatRoomRef.get(); const chatRoomData = chatRoomDoc.data(); const messagesPath = `${chatRoomRef.path}/messages`; // 校验用户是否为聊天室成员 if (!chatRoomData.members[userId]) { throw chatErrors.notChatRoomMember(); } // 删除聊天室所有消息 await deleteCollection(messagesPath); // 标记删除聊天室 await chatRoomRef.update({ deleted: true, lastMessage: admin.firestore.FieldValue.delete(), // 删除非范式存储的敏感数据 modifiedAt: admin.firestore.FieldValue.serverTimestamp(), read: admin.firestore.FieldValue.delete(), // 删除非范式存储的敏感数据 }); }
可以看到,我更新了modifiedAt和deleted两个字段。
监听器代码
我希望在客户端监听所有new Date()之后的聊天室变更(包括"lastMessage"字段更新、"read"字段更新、"删除"操作等),实现代码如下:
export function listenMyChats( startAt = undefined, onNext, onError ) { const currentUserId = getCurrentUser()?.uid; if (!currentUserId) { throw authErrors.authenticationRequired(); } let query = firestore .collection("chats") .where("membersArray", "array-contains", currentUserId) .orderBy("modifiedAt"); if (startAt) { query = query.startAt(startAt); } return query.onSnapshot(onNext, onError); }
我认为通过startAt和orderBy(modifiedAt)可以避免读取整个集合,逻辑是通顺的。
订阅代码
我通过如下逻辑订阅并处理聊天室变更:
const handleChatsChanges = async (querySnapshot) => { const changes = querySnapshot.docChanges(); await Promise.all( changes.map(async (change) => { if (change.type === "added") { // 新收到的聊天 console.log("Added"); } else if (change.type === "modified") { // 被修改的聊天(包含"deleted"、"read"、"lastMessage"等字段变更) console.log("Modified"); } }); ); }; useEffect(() => { const listener = listenMyChats( startAfter.current, handleMyChatsChanges, handleOnListenMyChatsError ); return () => { listener(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []);
我遇到的核心问题是:文档修改操作没有进入"modified"分支,而是触发了"added"事件。按照我的理解,监听器订阅时会将所有符合查询条件的文档以"added"事件返回,订阅完成后只有符合条件的新文档加入才会触发"added"事件,修改应该触发"modified"事件。为什么会出现修改操作被判定为新增事件的问题?
问题原因
这个问题是Firestore快照变更的判定逻辑和startAt参数的使用偏差共同导致的,核心原因有两点:
- 你在监听初始化时传入的
startAfter.current是页面挂载时的时间戳,而修改文档时会把modifiedAt更新为当前服务器时间,这个时间必然晚于页面挂载时间。修改前的文档modifiedAt早于你设置的startAt阈值,所以修改前的文档根本不在监听器的查询范围内,Firestore不会把它纳入监听池。 - 当你更新文档的
modifiedAt后,这个文档刚好满足了startAt(页面挂载时间)的过滤条件,首次进入监听器的查询结果集,Firestore就会判定这是一个新增的符合条件的文档,自然返回added类型的变更事件,而不是modified。
修复方案
你可以选择任意一种方案解决问题:
- 方案一:拆分查询逻辑,将历史数据拉取和实时变更监听分开。历史数据用分页查询一次性拉取,实时监听不设置
startAt阈值,初始化时得到的added事件全部过滤掉,只处理监听初始化完成之后的事件。 - 方案二:保留当前
startAt逻辑,但是调整事件判定规则:如果变更事件的文档ID已经存在于本地已加载的历史数据列表中,就算触发的是added事件,也按照modified逻辑处理。 - 方案三:新增专门的
createdAt字段用于区分历史和新增文档,监听时用where("createdAt", ">", 页面挂载时间)过滤新增文档,修改操作不会改动createdAt字段,原本就存在的文档修改后不会进入新增查询范围,自然会触发modified事件。
内容的提问来源于stack exchange,提问作者Victor Molina

