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

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参数的使用偏差共同导致的,核心原因有两点:

  1. 你在监听初始化时传入的startAfter.current是页面挂载时的时间戳,而修改文档时会把modifiedAt更新为当前服务器时间,这个时间必然晚于页面挂载时间。修改前的文档modifiedAt早于你设置的startAt阈值,所以修改前的文档根本不在监听器的查询范围内,Firestore不会把它纳入监听池。
  2. 当你更新文档的modifiedAt后,这个文档刚好满足了startAt(页面挂载时间)的过滤条件,首次进入监听器的查询结果集,Firestore就会判定这是一个新增的符合条件的文档,自然返回added类型的变更事件,而不是modified。

修复方案

你可以选择任意一种方案解决问题:

  • 方案一:拆分查询逻辑,将历史数据拉取和实时变更监听分开。历史数据用分页查询一次性拉取,实时监听不设置startAt阈值,初始化时得到的added事件全部过滤掉,只处理监听初始化完成之后的事件。
  • 方案二:保留当前startAt逻辑,但是调整事件判定规则:如果变更事件的文档ID已经存在于本地已加载的历史数据列表中,就算触发的是added事件,也按照modified逻辑处理。
  • 方案三:新增专门的createdAt字段用于区分历史和新增文档,监听时用where("createdAt", ">", 页面挂载时间)过滤新增文档,修改操作不会改动createdAt字段,原本就存在的文档修改后不会进入新增查询范围,自然会触发modified事件。

内容的提问来源于stack exchange,提问作者Victor Molina

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 07:54:05