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}]}
咨询问题:
- 是否存在配置遗漏以实现透明解析?
- 若无法通过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
相关产品推荐
相关产品推荐

