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

Firestore使用Batch Write更新关联数组时如何编写安全规则保证原子性

完整安全规则实现

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /students/{studentID} {
      allow read;
      allow create, update: if noChange('classes') || 
        (isFieldAdd('classes') && matchAdd(studentID, 'classes', 'students')) ||
        (isFieldRemove('classes') && matchRemove(studentID, 'classes', 'students'));
      allow delete: if checkDocDelete('classes', 'students');
    }
    
    match /classes/{classID} {
      allow read;
      allow create, update: if noChange('students') ||
        (isFieldAdd('students') && matchAdd(classID, 'students', 'classes')) ||
        (isFieldRemove('students') && matchRemove(classID, 'students', 'classes'));
      allow delete: if checkDocDelete('students', 'classes');
    }

    // 校验字段未变更
    function noChange(field) {
      return !(field in request.writeFields);
    }

    // 判断字段为新增关联ID场景
    function isFieldAdd(field) {
      return request.resource.data[field].removeAll(resource.data.get(field, [])).size() == 1;
    }

    // 判断字段为移除关联ID场景
    function isFieldRemove(field) {
      return resource.data.get(field, []).removeAll(request.resource.data[field]).size() == 1;
    }

    // 获取新增的关联ID
    function getAddedId(field) {
      return request.resource.data[field].removeAll(resource.data.get(field, []))[0];
    }

    // 获取移除的关联ID
    function getRemovedId(field) {
      return resource.data.get(field, []).removeAll(request.resource.data[field])[0];
    }

    // 新增场景校验:对面文档的关联数组已包含当前文档ID
    function matchAdd(currentDocId, localField, remoteField) {
      let remoteDocPath = /databases/$(database)/documents/$(localField)/$(getAddedId(localField));
      return currentDocId in getAfter(remoteDocPath).data.get(remoteField, []);
    }

    // 移除场景校验:对面文档的关联数组已不包含当前文档ID
    function matchRemove(currentDocId, localField, remoteField) {
      let remoteDocPath = /databases/$(database)/documents/$(localField)/$(getRemovedId(localField));
      return !(currentDocId in getAfter(remoteDocPath).data.get(remoteField, []));
    }

    // 整个文档删除校验:所有关联的对面文档都已移除当前文档ID
    function checkDocDelete(localField, remoteField) {
      let remoteIds = resource.data.get(localField, []);
      return remoteIds.size() == 0 || 
        remoteIds.all(id => !(request.resource.id in getAfter(/databases/$(database)/documents/$(localField)/$(id)).data.get(remoteField, [])));
    }
  }
}

逻辑说明

所有校验都基于getAfter()获取批量操作后的最终状态,符合批量操作的原子性要求,只要任意一侧校验不通过,整个批量会全部回滚:

  • 拆分了新增、移除、文档删除三个场景分别校验,完全覆盖要求的规则:
    • 数组修改必须同步修改对面:新增和移除场景都会校验对面的最终状态
    • 字段未变更不触发校验:noChange判断直接放行对应操作
    • 不可单独删除关联字段/单独删除文档:移除单个关联ID时校验对面已同步移除,删除整个文档时校验所有关联的对面文档都已移除当前文档的ID
  • 默认支持单条关联ID的新增/删除操作,如果你需要支持批量修改多个关联ID,只需修改对应辅助函数的判断逻辑,把size() == 1改成size() >=1,同时遍历所有变更的ID逐一校验即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 20:45:03