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

Apollo Client缓存read函数无法解析递归对象,求解决方案及最优实践

问题描述

后端采用如下GraphQL Schema定义树形结构,因无法实现动态递归,每个Node仅存储父节点ID而非父对象:

type Node {
  id: Int!
  parent_id: Int
}

type Query {
  nodes(): [Node!]
}

前端基于Angular框架使用Apollo Client查询所有节点,希望在JS中获得包含递归parent对象的结构:

interface Node {
  id: number;
  parent: Node|null;
}

尝试通过Apollo的type policies为Node添加本地字段parent,利用read函数将parent_id映射为父对象引用:

cache: new InMemoryCache({
  typePolicies: {
    Node: {
      fields: {
        parent: {
          read(_, {toReference, readField}) {
            if (!readField("parentId")) {
              return null;
            }

            return toReference({__typename: "Node", id: readField("parentId")})
          },
        },

但缓存返回的是__ref引用对象而非实际父对象:
查询语句:

query GetNodes {
  nodes {
    id
    parentId
    parent @client
  }
}

返回结果:

{"nodes":[{"__typename":"Node","parent":{"__ref":"Node:16"},"id":13,"parentId":16}]}

咨询问题:

  1. 是否存在配置遗漏以实现透明解析?
  2. 若无法通过Apollo缓存实现,是否有更规范的树形结构传输/转换方式,或应保持扁平结构按需查找?

解决方案

一、Apollo Client配置优化:自动解析引用

当前type policy返回的是缓存引用(__ref),Apollo默认不会自动展开引用对象。要实现透明解析,可通过以下两种方式调整:

1. 修改Type Policy的read函数,直接读取完整父节点

调整parent字段的read逻辑,从缓存中读取完整的父节点对象而非仅返回引用:

cache: new InMemoryCache({
  typePolicies: {
    Node: {
      fields: {
        parent: {
          read(_, { readField, cache }) {
            const parentId = readField("parentId");
            if (!parentId) return null;
            // 读取缓存中的完整父节点,递归包含parent字段以支持多层树形结构
            return cache.readFragment({
              id: cache.identify({ __typename: "Node", id: parentId }),
              fragment: gql`
                fragment ParentNode on Node {
                  id
                  parentId
                  parent @client
                }
              `,
            });
          },
        },
      },
    },
  },
});

2. 查询时显式嵌套@client字段

如果不想修改type policy,可在查询中显式嵌套parent @client字段,强制Apollo展开引用,但这种方式无法支持动态递归,需提前确定嵌套层级:

query GetNodes {
  nodes {
    id
    parentId
    parent @client {
      id
      parentId
      parent @client {
        id
        parentId
        # 按需继续嵌套到目标层级
      }
    }
  }
}

二、前端手动转换扁平数据为树形结构

若Apollo缓存配置无法满足需求,更灵活的方案是在前端拿到扁平节点列表后,手动转换为带递归parent的树形结构:

转换函数示例

interface FlatNode {
  id: number;
  parentId: number | null;
}

interface TreeNode {
  id: number;
  parent: TreeNode | null;
}

function convertToTree(nodes: FlatNode[]): TreeNode[] {
  // 构建节点映射表,快速查找节点
  const nodeMap = new Map<number, TreeNode>();
  nodes.forEach(node => {
    nodeMap.set(node.id, { id: node.id, parent: null });
  });

  // 为每个节点绑定父节点引用
  nodes.forEach(node => {
    if (node.parentId && nodeMap.has(node.parentId)) {
      nodeMap.get(node.id)!.parent = nodeMap.get(node.parentId)!;
    }
  });

  // 返回所有根节点(parentId为null的节点)
  return nodes.filter(node => !node.parentId).map(node => nodeMap.get(node.id)!);
}

使用方式:在Apollo查询结果回调中调用转换函数

this.apollo.watchQuery({ query: GET_NODES }).valueChanges.subscribe(({ data }) => {
  const treeNodes = convertToTree(data.nodes);
  // 使用转换后的树形结构进行业务逻辑处理
});

三、结构选择建议

  • 若树形结构层级不固定、需频繁访问父节点:优先选择前端手动转换,避免Apollo缓存配置的复杂性。
  • 若层级固定且查询模式单一:可尝试优化Apollo的type policy或查询语句,利用缓存自动展开引用。
  • 若树形结构庞大:建议保持扁平结构按需查找,避免一次性加载所有层级导致性能问题,需要访问父节点时直接通过parentId从缓存或映射表中查询。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 16:40:16