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

Apollo Client中GraphQL本地数据自动归一化实现方法咨询

Apollo本地缓存自动归一化:自动推断__typename与生成ID

问题背景

为优化性能(避免嵌套项变更触发全组件重绘),需要将GraphQL本地缓存归一化(扁平化嵌套结构)。原本以为Apollo会自动处理缓存归一化,但实际测试发现并非如此:

执行以下代码后:

const IS_LOGGED_IN = gql`
  query IsUserLoggedIn {
    isLoggedIn @client
    items @client {
      name @client
      quantity @client
    }
  }
`;

client.writeQuery({
  query: IS_LOGGED_IN,
  data: {
      isLoggedIn: false,
      items: [
          {name: "Oranges", quantity: "5"},
          {name: "Pears", quantity: "10"},
      ]
  },
});

开发者工具显示items数据未被归一化(仍保持嵌套结构),与从外部数据库查询的已归一化Location项形成明显差异。

手动为数据指定id和__typename可实现归一化,但操作繁琐:

const IS_LOGGED_IN = gql`
  query IsUserLoggedIn {
    isLoggedIn @client
    cake @client {
      name @client
      ingredients @client {
        name @client,
        quantity @client
      }
    }
  }
`;

client.writeQuery({
  query: IS_LOGGED_IN,
  data: {
      isLoggedIn: false,
      cake: [
          {id: "1", __typename:"Cake", name: "Cookies", ingredients: [
              {id: "4", __typename:"Ingredients", name: "Sugar", quantity: "100g"},
              {id: "5", __typename:"Ingredients", name: "Chocolate", quantity: "250g"}
          ]},
          {id: "2", __typename:"Cake", name: "Choux", ingredients: [
              {id: "6", __typename:"Ingredients", name: "Eggs", quantity: "2"},
              {id: "8", __typename:"Ingredients", name: "Flour", quantity: "250g"}
          ]},
      ]
  },
});

尝试通过typePolicies生成随机ID,但担心破坏cache.identify功能:

const client = new ApolloClient({
    cache: new InMemoryCache({
        typePolicies: {
            Ingredients: {
                // If the ID can be deduced from the fields
                //keyFields: ['name'],
                // If the ID must be different for each elements
                // I hope that using randomness here is not an issue…
                // in particular I am not sure if it does not break
                // functions to identify the ID of an element…
                keyFields: () => "Ingredients:" + uuidv4(),
            },
        },
    }),
});

同时无法避免手动输入__typename,因此想知道:处理GraphQL本地数据时,能否自动从查询推断__typename并生成ID,减少手动操作与错误?


解决方案

1. 自动注入__typename

Apollo Client的addTypename默认开启,但仅对发送给服务端的查询生效。针对本地数据,可以通过以下方式自动注入:

方法一:工具函数批量添加

封装通用函数,为嵌套数据自动添加__typename:

const addTypenames = (data, typename, nestedMap = {}) => {
  const processed = { ...data, __typename: typename };
  // 处理指定的嵌套字段
  Object.entries(nestedMap).forEach(([key, childTypename]) => {
    if (processed[key]) {
      processed[key] = Array.isArray(processed[key])
        ? processed[key].map(item => addTypenames(item, childTypename, nestedMap[key] || {}))
        : addTypenames(processed[key], childTypename, nestedMap[key] || {});
    }
  });
  return processed;
};

// 使用示例
const cakeData = [
  {name: "Cookies", ingredients: [
    {name: "Sugar", quantity: "100g"},
    {name: "Chocolate", quantity: "250g"}
  ]},
  {name: "Choux", ingredients: [
    {name: "Eggs", quantity: "2"},
    {name: "Flour", quantity: "250g"}
  ]},
].map(item => addTypenames(item, 'Cake', { ingredients: 'Ingredients' }));

client.writeQuery({
  query: IS_LOGGED_IN,
  data: { isLoggedIn: false, cake: cakeData }
});

方法二:自定义链接拦截写入操作

通过Apollo Link拦截writeQuery/writeFragment操作,自动为数据添加__typename:

import { ApolloLink } from '@apollo/client';

const autoTypenameLink = new ApolloLink((operation, forward) => {
  const context = operation.getContext();
  // 仅处理本地写入操作
  if (context.operationName === 'IsUserLoggedIn') {
    const data = context.data || operation.variables?.data;
    if (data?.cake) {
      data.cake = data.cake.map(cake => ({
        ...cake,
        __typename: 'Cake',
        ingredients: cake.ingredients.map(ing => ({ ...ing, __typename: 'Ingredients' }))
      }));
    }
    operation.setContext({ data });
  }
  return forward(operation);
});

// 注册到ApolloClient链接链
const client = new ApolloClient({
  link: ApolloLink.from([autoTypenameLink, /* 其他链接 */]),
  cache: new InMemoryCache(...)
});

2. 自动生成ID且不破坏缓存识别

绝对不要用随机函数作为keyFields的返回值——keyFields的作用是提取实体的唯一标识,每次生成新ID会导致缓存无法识别同一实体,完全失去归一化意义。正确的自动生成方式:

方法一:本地Schema解析器生成ID

定义本地GraphQL Schema,在解析器中自动为无ID的实体生成UUID:

import { makeExecutableSchema } from '@graphql-tools/schema';
import { v4 as uuidv4 } from 'uuid';

const typeDefs = `
  type Cake {
    id: ID!
    name: String!
    ingredients: [Ingredients!]!
  }
  type Ingredients {
    id: ID!
    name: String!
    quantity: String!
  }
  type Query {
    isLoggedIn: Boolean!
    cake: [Cake!]!
  }
`;

const resolvers = {
  Cake: {
    id: (parent) => parent.id || uuidv4()
  },
  Ingredients: {
    id: (parent) => parent.id || uuidv4()
  },
  // 其他解析器...
};

const localSchema = makeExecutableSchema({ typeDefs, resolvers });

后续通过writeQuery写入数据时,无需手动指定ID,解析器会自动补充。

方法二:写入前批量生成ID

封装工具函数,在写入缓存前为数据批量生成ID:

import { v4 as uuidv4 } from 'uuid';

const generateIds = (data, typename, nestedMap = {}) => {
  const processed = {
    ...data,
    id: data.id || `${typename}_${uuidv4()}`,
    __typename: typename
  };
  // 处理嵌套字段
  Object.entries(nestedMap).forEach(([key, childTypename]) => {
    if (processed[key]) {
      processed[key] = Array.isArray(processed[key])
        ? processed[key].map(item => generateIds(item, childTypename, nestedMap[key] || {}))
        : generateIds(processed[key], childTypename, nestedMap[key] || {});
    }
  });
  return processed;
};

// 使用示例
const cakeData = [
  {name: "Cookies", ingredients: [
    {name: "Sugar", quantity: "100g"},
    {name: "Chocolate", quantity: "250g"}
  ]},
  {name: "Choux", ingredients: [
    {name: "Eggs", quantity: "2"},
    {name: "Flour", quantity: "250g"}
  ]},
].map(item => generateIds(item, 'Cake', { ingredients: 'Ingredients' }));

client.writeQuery({
  query: IS_LOGGED_IN,
  data: { isLoggedIn: false, cake: cakeData }
});

3. 配置缓存策略确保归一化

确保InMemoryCache的typePolicies正确指定每个类型的主键字段,让Apollo能正确识别实体:

const client = new ApolloClient({
  cache: new InMemoryCache({
    typePolicies: {
      Cake: {
        keyFields: ['id'], // 明确指定主键为id
      },
      Ingredients: {
        keyFields: ['id'],
      },
    },
  }),
});

总结

  • 禁用随机生成keyFields的方式,避免破坏缓存识别逻辑;
  • 通过工具函数或本地Schema自动生成ID与注入__typename,减少手动重复操作;
  • 正确配置typePolicies的主键字段,确保Apollo能完成缓存归一化。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 15:12:33