从10月8日至10日在纽约市加入我们,了解GraphQL联盟和API平台工程的最新技巧、趋势和新闻。加入我们参加2024年纽约市的GraphQL峰会
文档
免费开始

读取和写入数据到缓存


您可以直接将数据读取和写入到 缓存,无需与您的 进行通信。您可以使用以前从您的服务器获取的数据,以及 只在本地可用的数据。本地

Apollo 客户端 支持多种策略来与缓存数据交互:

策略API描述
使用 GraphQL 查询readQuery / writeQuery / updateQuery使用标准 GraphQL 查询管理远程和本地数据。
使用 GraphQL 片段readFragment / writeFragment / updateFragment / useFragment直接访问任何缓存对象的字段,而无需构建完整的查询。
直接修改缓存字段cache.modify无需使用 GraphQL 即可操作缓存数据。

您可以使用最适合您用例的策略和方法组合。

请记住,包含引用其他对象的缓存字段和包含直接值的字段之间的区别。是指包含一个__ref字段的对象——请参见缓存概述中的示例

以下所有代码示例都假定您已初始化了ApolloClient的实例,并且已从@apollo/client导入了gql函数。

在一个React组件中,您可以使用ApolloProvider组件和useApolloClient钩子访问您的ApolloClient实例。

使用 GraphQL 查询

您可以使用与在服务器上执行的查询类似或完全相同的查询读取和写入缓存数据:

readQuery

`readQuery` 方法允许您直接在缓存中执行一个 GraphQL 查询,如下所示:

const READ_TODO = gql`
query ReadTodo($id: ID!) {
todo(id: $id) {
id
text
completed
}
}
`;
// Fetch the cached to-do item with ID 5
const { todo } = client.readQuery({
query: READ_TODO,
// Provide any required variables in this object.
// Variables of mismatched types will return `null`.
variables: {
id: 5,
},
});

如果您的缓存包含了查询中所有字段的全部数据,readQuery将返回一个与查询形状相符的对象。

要成功使用 执行查询,指定的 字段 必须已经在缓存中。在上述示例中,要获取 ID 为 5 的待办事项,必须已经缓存 todo 字段(s),对应 id:5。对于本例,缓存看起来可能像这样:

{
ROOT_QUERY: {
'todo({"id":5})': {
__ref: 'Todo:5'
}
},
'Todo:5': {
// ...
}
}

否则,客户端会将数据视为丢失,readQuery 返回 null。要了解更多关于缓存的工作原理,请访问 缓存概述

{
todo: {
__typename: 'Todo', // __typename is automatically included
id: 5,
text: 'Buy oranges 🍊',
completed: true
}
}

Apollo Client 默认会自动查询每个对象的 __typename,即使您在查询字符串中不包括此字段。

请不要直接修改返回的对象。 同一个对象可能会被多个组件返回。要安全地更新缓存数据,请参阅 合并读取和写入

如果查询的字段数据在缓存中缺失,anyreadQuery 返回 null。它不会尝试从您的 GraphQL 服务器获取数据。

您提供的 readQuery 查询可以包含您的 GraphQL 服务器schema(即 本地字段)中未定义的字段。

Apollo Client 3.3 之前,readQuery 抛出了 MissingFieldError 异常来报告缺失的字段。从 Apollo Client 3.3 开始,readQuery 总是返回 null 来指示字段缺失。

writeQuery

writeQuery 方法允许您将数据以与 GraphQL 查询匹配的形状写入缓存。它与 readQuery 相似,但需要指定一个 data 选项:

client.writeQuery({
query: gql`
query WriteTodo($id: Int!) {
todo(id: $id) {
id
text
completed
}
}`,
data: { // Contains the data to write
todo: {
__typename: 'Todo',
id: 5,
text: 'Buy grapes 🍇',
completed: false
},
},
variables: {
id: 5
}
});

此示例创建(或编辑)了一个具有 ID 5 的缓存 Todo 对象。

以下 writeQuery 的注意事项:

  • 您使用 writeQuery 对缓存数据所做的任何更改 不会 推送到您的 GraphQL 服务器。如果您重新加载环境,这些更改将消失。
  • 您的查询形状不由您的 GraphQL 服务器schema强制执行:
    • 查询可以包含您的 schema 中 不存在 的字段。
    • 您可以(但通常 不应)为主 schema 字段提供值,这些值的 schema 是不可见的。

编辑现有数据

在上面的例子中,如果您的缓存已经 包含一个具有 ID5Todo 对象,writeQuery 将会覆盖 data 中包含的字段(其他字段将被保留):

// BEFORE
{
'Todo:5': {
__typename: 'Todo',
id: 5,
text: 'Buy oranges 🍊',
completed: true,
dueDate: '2022-07-02'
}
}
// AFTER
{
'Todo:5': {
__typename: 'Todo',
id: 5,
text: 'Buy grapes 🍇',
completed: false,
dueDate: '2022-07-02'
}
}

如果您在 query 中包含一个 value,但是未在 data 中包含它的值,该字段的当前缓存值将保留。

使用 GraphQL 片段

您可以使用 GraphQL 从任何规范化的缓存对象中读取和写入缓存数据。这比 readQuery/writeQuery 提供更多的“随机访问”到您的缓存数据,因为 readQuery/writeQuery 需要一个完整的有效 query

readFragment

此示例使用 readFragment 而不是 示例的 readQuery 获取相同的数据:

readQuery 不同,readFragment 需要一个 id(ID)选项。此选项指定缓存中对象的缓存 ID。 默认情况下,缓存 IDs 的格式为 <__typename>:<id>(这是为什么我们在上面的示例中提供 Todo:5)。您可以 自定义此 ID。

在上面的例子中,以下两种情况之一,readFragment 返回 null

  • 不存在具有 ID 5 的缓存 Todo 对象。
  • 存在具有 ID is 的缓存 Todo 对象,但它缺少 textcompleted 的值。

在 Apollo Client 3.3 之前,readFragment � throw MissingFieldError 异常来报告缺失的字段,并且只有在从不存在的 ID 中读取片段时才会返回 null。从 Apollo Client 3.3 开始,readFragment 总是返回 null 来表示数据不足(缺少 ID 或缺少字段),而不是抛出 MissingFieldError

writeFragment

除了可以使用 Apollo Client 缓存中的 readFragment 从缓存中读取随机访问的数据外,您还可以使用 writeFragment 方法向缓存写入数据。

您使用 writeFragment 对缓存数据所做的任何更改都不会推送到您的 GraphQL 服务器。 如果您重新加载环境,这些更改将消失。

"writeFragment" 方法与 readFragment 类似,但它需要一个额外的 data 变量。例如,以下对 writeFragment 的调用将更新具有 id5Todo 对象的 completed 标志:

client.writeFragment({
id: 'Todo:5',
fragment: gql`
fragment MyTodo on Todo {
completed
}
`,
data: {
completed: true,
},
});

所有订阅 Apollo Client 缓存的订阅者(包括所有活动查询)都会看到这个变化,并相应地更新您的应用程序 UI。

watchFragment
因为3.10.0

根据指定的选项监视 fragment 的缓存存储,并返回一个 Observable。我们可以订阅这个 Observable 并在缓存存储更改时通过观察者接收更新结果。

您必须传递一个包含单个 fragment 的 GraphQL 或包含表示要读取内容的多片段文档。如果传递带有多个片段的文档,则还必须指定一个 fragmentName

useFragment
因为3.8.0

您可以使用 useFragment 钩子直接从缓存读取给定 fragment 的数据。此钩子返回对缓存当前包含的给定 fragment 的始终最新的视图。查看 API 参考。

组合读取和写入

您可以将 readQuerywriteQuery(或 readFragmentwriteFragment)组合起来检索当前缓存的数据库并对其进行选择性修改。下面示例创建了一个新的 Todo 项目并将其添加到您的缓存待办事项列表中。请记住,这次添加不会发送到您的远程服务器。

为了方便,您可以使用 cache.updateQuerycache.updateFragment 通过单个方法调用将读取和写入缓存数据组合起来:

// Query to fetch all todo items
const query = gql`
query MyTodoAppQuery {
todos {
id
text
completed
}
}
`;
// Set all todos in the cache as completed
cache.updateQuery({ query }, (data) => ({
todos: data.todos.map((todo) => ({ ...todo, completed: true }))
}));

这些方法各自接受两个参数

  • 与它的 read 方法对应的相同 options 参数(它始终包括一个 queryfragment
  • 一个 更新函数

无论采用哪种方法从缓存中获取数据,它都会调用其更新函数并将缓存的data传递给它。update函数可以返回一个值来替换缓存中的data。在上述示例中,每个缓存的Todo对象的completed字段都被设置为true(其他字段保持不变)。

请注意,替换值必须以不可变的方式进行计算。您可以在React 文档中了解更多关于不可变更新的信息。

如果更新函数不应该更改缓存的任何数据,则可以返回undefined

更新函数的返回值将被传递给writeQuerywriteFragment,以修改缓存数据。

请参阅cache.updateQuerycache.updateFragment的完整API参考。

使用cache.modify

InMemoryCachemodify方法允许您直接修改单个缓存字段的值,甚至可以完全删除字段。

  • writeQuerywriteFragment类似,modify会触发所有依赖于修改字段的活跃查询的刷新(除非您通过传递broadcast: false来覆盖此行为)。
  • writeQuerywriteFragment不同:
    • modify绕过了您定义的任何merge函数,这意味着字段总是被您指定的确切值所覆盖。
    • modify无法写入缓存中尚不存在的字段。
  • 受监视的查询可以通过向client.watchQueryuseQuery钩子传递如fetchPolicynextFetchPolicy之类的选项来控制当缓存更新导致它们无效时会发生什么。

参数

API参考中官方文档中,modify方法接受以下参数:

  • 要修改的缓存对象ID(我们建议使用 cache.identify 来获取)
  • 要执行的解释函数映射(每个要修改的字段一个)
  • 可选的 broadcastoptimistic 布尔值,以自定义行为

修改函数作用于单个 field。它接收关联字段的当前缓存值作为参数,并返回任何应替换它的值。

这里是调用 modify 的一个示例,该调用修改了 name 字段,将其值转换为大写:

cache.modify({
id: cache.identify(myObject),
fields: {
name(cachedName) {
return cachedName.toUpperCase();
},
},
/* broadcast: false // Include this to prevent automatic query refresh */
});

如果您没有为特定 field 提供修改函数,则该 field 的缓存值保持不变。

值与引用

当您为包含标量、枚举或这些基本类型列表的字段定义修改函数时,修改函数将传递该字段的现有确切值。例如,如果您为具有当前值 5 的对象的 quantity 字段定义修改函数,则您的修改函数将传递值 5

但是,当您为包含对象类型或对象列表的字段定义修改函数时,这些对象表示为 引用。每个引用通过其缓存 ID 指向缓存的相应对象。如果您的修改函数返回一个 不同的 引用,您将更改该字段中包含的其他缓存对象。您 不会 修改原始缓存对象的数据。此外,修改引用中字段(或添加新字段)只会影响您正在修改的位置。

修改函数实用工具

修改函数可选地接受第二个参数,它是一个包含几个有用实用工具的对象。

以下示例中使用了这些实用工具中的一些(即,readField 函数和 DELETE 信号对象)。有关所有可用实用工具的说明,请参阅 API 参考

示例

示例:从列表中删除一个项

假设我们有一个博客应用程序,其中每个 Post 都有一个 Comment 的数组。这是我们从分页的 Post.comments 数组中删除特定 Comment 的方法:

const idToRemove = 'abc123';
cache.modify({
id: cache.identify(myPost),
fields: {
comments(existingCommentRefs, { readField }) {
return existingCommentRefs.filter(
commentRef => idToRemove !== readField('id', commentRef)
);
},
},
});

让我们分析一下

  • id 字段中,我们使用 cache.identify 来获取要从中删除注释的缓存 Post 对象的缓存 ID。

  • fields 字段中,我们提供一个对象,列出我们的修改函数。在这种情况下,我们定义了一个单个修改函数(针对 comments 字段)。

  • 注释修饰函数接受我们的现有缓存的评论数组的参数(existingCommentRefs)。它还使用 readField 工具函数,该函数有助于您读取任何缓存的字段的值。

  • 修饰函数返回一个数组,该数组过滤出所有具有匹配 ID idToRemove 的注释。返回的数组替换缓存中的现有数组。

示例:向列表添加项目

现在让我们来看一下如何在 Post 中添加一个 Comment

const newComment = {
__typename: 'Comment',
id: 'abc123',
text: 'Great blog post!',
};
cache.modify({
id: cache.identify(myPost),
fields: {
comments(existingCommentRefs = [], { readField }) {
const newCommentRef = cache.writeFragment({
data: newComment,
fragment: gql`
fragment NewComment on Comment {
id
text
}
`
});
// Quick safety check - if the new comment is already
// present in the cache, we don't need to add it again.
if (existingCommentRefs.some(
ref => readField('id', ref) === newComment.id
)) {
return existingCommentRefs;
}
return [...existingCommentRefs, newCommentRef];
}
}
});

当调用 comments 字段修饰函数时,它首先调用 writeFragment 以将我们的 newComment 数据存储在缓存中。函数 writeFragment 返回一个引用(newCommentRef),指向新缓存的评论。

作为安全检查,我们扫描现有的评论引用数组(existingCommentRefs),以确保我们的新评论尚未在列表中。如果没有,我们将新的评论引用添加到引用列表中,并将完整的列表存储在缓存中。

示例:在变更后更新缓存

如果您使用包含缓存可以识别的 options.data 对象调用 writeFragment(基于其 __typename 和缓存 ID 字段),则可以避免传递 options.idwriteFragment

无论您是否显式提供 options.id 或让 writeFragment 使用 options.data 计算它,writeFragment 都返回指向标识对象的 Reference

这种行为使得 writeFragment 成为获取缓存中现有对象引用的好工具,这在编写 useMutationupdate 函数时很有用:

例如

const [addComment] = useMutation(ADD_COMMENT, {
update(cache, { data: { addComment } }) {
cache.modify({
id: cache.identify(myPost),
fields: {
comments(existingCommentRefs = [], { readField }) {
const newCommentRef = cache.writeFragment({
data: addComment,
fragment: gql`
fragment NewComment on Comment {
id
text
}
`
});
return [...existingCommentRefs, newCommentRef];
}
}
});
}
});

在这个例子中,useMutation 自动创建了一个 Comment 并将其添加到缓存中,但它不会自动知道如何将该 Comment 添加到对应的 Post 的评论列表中。这意味着监视 Post 的评论列表的任何查询都不会更新。

为解决这一问题,我们使用 update 回调像前面的例子一样,我们将新的评论添加到列表中。与前面的例子不同的是,评论已被 useMutation 添加到缓存中。因此,cache.writeFragment 返回现有对象的引用。

示例:从缓存的实体中删除字段

修饰函数的可选第二个参数是一个包含 几个实用工具函数,如 canReadisReference 函数。它还包括一个名为 DELETE 的哨兵对象。

要从一个特定的缓存实体中删除一个 字段,从该字段的修饰函数中返回 DELETE 哨兵对象,如下所示:

cache.modify({
id: cache.identify(myPost),
fields: {
comments(existingCommentRefs, { DELETE }) {
return DELETE;
},
},
});

示例:使缓存对象中的字段无效化

通常,更改或删除字段的值也会 使字段无效化,如果它们之前读取过该字段,则会重新读取监视查询。

使用 cache.modify,还可以通过返回 INVALIDATE 哨兵来使 字段无效化,而不改变或删除其值。

cache.modify({
id: cache.identify(myPost),
fields: {
comments(existingCommentRefs, { INVALIDATE }) {
return INVALIDATE;
},
},
});

如果需要使给定对象中的所有 字段无效,可以将修饰函数作为 fields 选项的值传递:

cache.modify({
id: cache.identify(myPost),
fields(fieldValue, details) {
return details.INVALIDATE;
},
});

在使用这种形式的 cache.modify 时,您可以使用 details.fieldName 确定单个 字段 名称。

获取实体的缓存 ID

如果你的缓存中使用了一个 自定义缓存 ID(即使没有使用,也可以使用)cache.identify 方法来获取该类型的对象的缓存 ID。该方法接受一个对象,并根据它的 __typename 和标识符字段以计算其 ID。这意味着你不需要跟踪构成每种类型缓存 ID 的字段。

示例

假设我们有一个缓存 GraphQL 对象的 JavaScript 表示,如下所示:

const invisibleManBook = {
__typename: 'Book',
isbn: '9780679601395', // The key field for this type's cache ID
title: 'Invisible Man',
author: {
__typename: 'Author',
name: 'Ralph Ellison',
},
};

如果我们想在使用 writeFragmentcache.modify 等方法与我们的缓存中的这个对象进行交互时,我们需要得到这个对象的缓存 ID。我们的 Book 类型似乎有一个自定义的缓存 ID,因为我们没有看到 id 字段。

而不是需要查找我们Book类型的缓存ID使用isbn 字段,我们可以使用cache.identify方法,如下所示:

const bookYearFragment = gql`
fragment BookYear on Book {
publicationYear
}
`;
const fragmentResult = cache.writeFragment({
id: cache.identify(invisibleManBook),
fragment: bookYearFragment,
data: {
publicationYear: '1952'
}
});

缓存知道Book类型使用isbn 字段作为其缓存ID,因此cache.identify可以正确地填充上面的id字段。

此示例很简单,因为我们的缓存ID使用单个字段isbn)。但是自定义的缓存ID可以由多个多个字段组成(例如isbn标题)。这使得在不使用cache.identify的情况下指定对象的缓存ID变得更具挑战性和重复性。

上一页
配置
下一页
垃圾回收和驱逐
评分文章评分在GitHub上编辑编辑论坛Discord

©2024Apollo Graph Inc.,以Apollo GraphQL的名义。

隐私政策

公司