读取和写入数据到缓存
您可以直接将数据读取和写入到 Apollo 客户端 缓存,无需与您的 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 查询
您可以使用与在服务器上执行的查询类似或完全相同的GraphQL查询读取和写入缓存数据:
readQuery
`readQuery` 方法允许您直接在缓存中执行一个 GraphQL 查询,如下所示:
const READ_TODO = gql`query ReadTodo($id: ID!) {todo(id: $id) {idtextcompleted}}`;// Fetch the cached to-do item with ID 5const { 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 includedid: 5,text: 'Buy oranges 🍊',completed: true}}
Apollo Client 默认会自动查询每个对象的 __typename
,即使您在查询字符串中不包括此字段。
请不要直接修改返回的对象。 同一个对象可能会被多个组件返回。要安全地更新缓存数据,请参阅 合并读取和写入。
如果查询的字段数据在缓存中缺失,any,readQuery
返回 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) {idtextcompleted}}`,data: { // Contains the data to writetodo: {__typename: 'Todo',id: 5,text: 'Buy grapes 🍇',completed: false},},variables: {id: 5}});
此示例创建(或编辑)了一个具有 ID 5
的缓存 Todo
对象。
以下 writeQuery
的注意事项:
- 您使用
writeQuery
对缓存数据所做的任何更改 不会 推送到您的 GraphQL 服务器。如果您重新加载环境,这些更改将消失。 - 您的查询形状不由您的 GraphQL 服务器schema强制执行:
- 查询可以包含您的 schema 中 不存在 的字段。
- 您可以(但通常 不应)为主 schema 字段提供值,这些值的 schema 是不可见的。
编辑现有数据
在上面的例子中,如果您的缓存已经 包含一个具有 ID5
的 Todo
对象,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
获取相同的数据:
const todo = client.readFragment({id: 'Todo:5', // The value of the to-do item's cache IDfragment: gql`fragment MyTodo on Todo {idtextcompleted}`,});
与 readQuery
不同,readFragment
需要一个 id
(ID)选项。此选项指定缓存中对象的缓存 ID。 默认情况下,缓存 IDs 的格式为 <__typename>:<id>
(这是为什么我们在上面的示例中提供 Todo:5
)。您可以 自定义此 ID。。
在上面的例子中,以下两种情况之一,readFragment
返回 null
:
- 不存在具有 ID
5
的缓存Todo
对象。 - 存在具有 ID is 的缓存
Todo
对象,但它缺少text
或completed
的值。
在 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
的调用将更新具有 id
为 5
的 Todo
对象的 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 参考。
组合读取和写入
您可以将 readQuery
和 writeQuery
(或 readFragment
和 writeFragment
)组合起来检索当前缓存的数据库并对其进行选择性修改。下面示例创建了一个新的 Todo
项目并将其添加到您的缓存待办事项列表中。请记住,这次添加不会发送到您的远程服务器。
为了方便,您可以使用 cache.updateQuery
或 cache.updateFragment
通过单个方法调用将读取和写入缓存数据组合起来:
// Query to fetch all todo itemsconst query = gql`query MyTodoAppQuery {todos {idtextcompleted}}`;// Set all todos in the cache as completedcache.updateQuery({ query }, (data) => ({todos: data.todos.map((todo) => ({ ...todo, completed: true }))}));
这些方法各自接受两个参数
- 与它的
read
方法对应的相同options
参数(它始终包括一个query
或fragment
) - 一个 更新函数
无论采用哪种方法从缓存中获取数据,它都会调用其更新函数并将缓存的data
传递给它。update
函数可以返回一个值来替换缓存中的data
。在上述示例中,每个缓存的Todo
对象的completed
字段都被设置为true
(其他字段保持不变)。
请注意,替换值必须以不可变的方式进行计算。您可以在React 文档中了解更多关于不可变更新的信息。
如果更新函数不应该更改缓存的任何数据,则可以返回undefined
。
更新函数的返回值将被传递给writeQuery
或writeFragment
,以修改缓存数据。
请参阅cache.updateQuery
和cache.updateFragment
的完整API参考。
使用cache.modify
InMemoryCache
的modify
方法允许您直接修改单个缓存字段的值,甚至可以完全删除字段。
- 与
writeQuery
和writeFragment
类似,modify
会触发所有依赖于修改字段的活跃查询的刷新(除非您通过传递broadcast: false
来覆盖此行为)。 - 与
writeQuery
和writeFragment
不同:modify
绕过了您定义的任何merge
函数,这意味着字段总是被您指定的确切值所覆盖。modify
无法写入缓存中尚不存在的字段。
- 受监视的查询可以通过向
client.watchQuery
或useQuery
钩子传递如fetchPolicy
和nextFetchPolicy
之类的选项来控制当缓存更新导致它们无效时会发生什么。
参数
在API参考中官方文档中,modify
方法接受以下参数:
- 要修改的缓存对象ID(我们建议使用
cache.identify
来获取) - 要执行的解释函数映射(每个要修改的字段一个)
- 可选的
broadcast
和optimistic
布尔值,以自定义行为
修改函数作用于单个 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 {idtext}`});// 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.id
到 writeFragment
。
无论您是否显式提供 options.id
或让 writeFragment
使用 options.data
计算它,writeFragment
都返回指向标识对象的 Reference
。
这种行为使得 writeFragment
成为获取缓存中现有对象引用的好工具,这在编写 useMutation
的 update
函数时很有用:
例如
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 {idtext}`});return [...existingCommentRefs, newCommentRef];}}});}});
在这个例子中,useMutation
自动创建了一个 Comment
并将其添加到缓存中,但它不会自动知道如何将该 Comment
添加到对应的 Post
的评论列表中。这意味着监视 Post
的评论列表的任何查询都不会更新。
为解决这一问题,我们使用 update
回调。像前面的例子一样,我们将新的评论添加到列表中。与前面的例子不同的是,评论已被 useMutation 添加到缓存中。因此,cache.writeFragment 返回现有对象的引用。
示例:从缓存的实体中删除字段
修饰函数的可选第二个参数是一个包含 几个实用工具函数,如 canRead
和 isReference
函数。它还包括一个名为 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 IDtitle: 'Invisible Man',author: {__typename: 'Author',name: 'Ralph Ellison',},};
如果我们想在使用 writeFragment
或 cache.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变得更具挑战性和重复性。