Intro
Apollo 作為 graphql engine 提供了非常便利的 cache 機制,然而在 cahce 提供的便利性之外,卻也導致後續的 request 及 graphql 錯誤讀取 cache 中資料的可能性,產生資料無法正確更新的問題。本文將基於我最近的經驗與理解做基礎的介紹,並試圖釐清作為初學者所遇到的問題。
作為一個新接觸 ApolloServer 的使用者,其中一個問題是該怎麼理解 apollo 所提供的 Cache。
在初步讀完 document 之後,可以發現 apollo 在 ApolloServer 端提供 Cache (Redis, Memcached, etc.) 的同時,也在 ApolloClient 端提供了 InMemoryCahce
,如何理解兩者與前端的互動就成為了第一個頭痛的點。
ApolloClient & InMemoryCache
InMemoryCache
既然是 Cache,那必然提供我們緩存資料的功能。沒錯,透過InMemoryCache
,Clinet 端的可以透過 Cache 去紀錄使用者發送給 Server 的 GraphqQL query,並將query 與其回傳的資料緩存起來,當使用者再次發送同樣的 query 時, apollo 並不會實際送一個 request 到前端,而是從 Cahce 中去讀取資料。藉有減少發送請求的數量去加快在前端的反應速度。
const GET_MOVIE = gql`
mutation getMovie($id: Int!) {
getMovie(id: $id) {
name
}
}
`;
const UPDATE_MOVIE = gql`
mutation UpdateMovie($id: Int!, $name: String!) {
updateMovie(id: $id, name: $name) {
id
name
}
}
`;
這邊提一點,InMemoryCache
會透過發送 query 的 __typename__
,以及 id
去生成一組 ID。因此當 Client 端發送上列的 Query 去拿取 ID 為 12 的 Movie 時 ,Cache 會為 Movie 生成一組 Movie:12
,並在後續的 query 與這組 ID 有關時,透過其去查找Cache 中的資料。若發送的是 mutation,也能夠藉此更新存在 cache 中的資料。
因此當我們分別發送上列的 query 與 mutation 時,apollo 會先從 server 中取得資料並存入 cache,而在進行 mutation 的時候,發送 request 到 server 端並用 Movie:12
去更新存在 Cache 中的資料,節省手動操作的麻煩。
然而,上述的特性在某些特殊情形下會無法正確的更新的 Cache,這裡不多贅述,有興趣的朋友可以參照 Umbrella issue。因為有這些特殊情形,apollo 也提供了 API 去操作 Cache,以下列出兩種我嘗試過的 API 給大家參考
// update: modify cache with readQuery and writeQuery
apollClient.mutate({
mutation: UPDATE_MOVIES,
variables: {
name
},
update: (apollClient, { data: { addMovie } }) => {
// Read the data from our cache for this query.
const data = apollClient.readQuery({ query: getMovies });
// Add our movie from the mutation to the end.
data.movies.push(addMovie);
// Write our data back to the cache.
apollClient.writeQuery({ query: getMovies, data });
}
})
// refetchQuery: query after mutation, passed an array of queries
apollClient.mutate({
mutation: GET_MOVIES,
variables: {
name
},
refetchQuery: [
{
query: GET_MOVIES
}
]
})
這邊必須注意,InMemoryCache
顧名思義是 in memory,所以在前端 refresh 過後 writeQuery
修改的資料就會消失!
最後,InMemoryCache
的設置方面十分容易,可以直接在 ApolloClinet 中建立
import { InMemoryCache, ApolloClient } from '@apollo/client';
const client = new ApolloClient({
// ...other arguments...
cache: new InMemoryCache(options)
});
ApolloServer & HTTP cacheing
開門見山,在文擋的副標題有這麼一句: Automatically set HTTP cache headers and save full responses in a cache ,直截了當告訴你 ApolloServer 支援 HTTP 的 Cache Header,而究竟是怎麼個支持法,讓我們看下去。
首先我們可以看到 ApolloServer 提供了以下的介面
const server = new ApolloServer({
typeDefs,
resolvers,
cache: new MemcachedCache(
['memcached-server-1', 'memcached-server-2', 'memcached-server-3'],
{ retries: 10, retry: 10000 }, // Options
),
dataSources: () => ({
moviesAPI: new MoviesAPI(),
}),
});
class MoviesAPI extends RESTDataSource {
public getMovie(id) {
this.get(`/v1/movie/${id}`, null, { cacheOptions: { ttl: 60 } })
}
其中最大的關鍵在於 cacheOptions
, cacheOptions
存在的目的是讓 HTTP request 能夠在 header 中加入 Cache-Control
,進而做到 cache response 的效果。在此可以理解 ApolloServer 端的 cahce 是為 RestDataSource 而設計,使其能夠在給定的 ttl 有效期間內,讓從 server 端提取的資料不需要額外發送一個 request,可以從現存的 response 提取。
雖然這很方便但也對我們的應用造成了下面這種情形:
1. query page (GET v1/movie/:id)
2. mutation (PATCH v1/movie/:id)
3. refresh page (GET v1/movie/:id)
按照步驟操作後,會發現在第三步重新整理過後,mutation 的結果並沒有成功地顯示在前端,儘管資料可能已經正確的存在DB當中了。
同樣的為了避免這種情況,可以透過在 Server 去確認 request 並針對想要 invalidate 的 request 去做設計。例如在資料被修改過的情形下,把 GET 回傳的資料從 Cache 中刪除,例如可以透過 Context 以及 Id 去做操作。
Automatic Persisted Queries
看完這部分之後,大家可能會發現還有一個類似 Cache 的東西(至少我覺得)叫做 Automatic Persisted Query,簡稱為APQ。
APQ 的存在主要是為了處理 GraphQL query 大小太大導致的延遲問題,其會將一個 query 包成 hash 傳送給介於 Apollo Server 及 Client 中間的 cache layer,並透過確認是否有hash key 來決定是否要發送完整的 graphql query 給 server,因此無論如何再 server 端都會執行 query,而其 cache 的並不是回傳的資料本身,而是從 client 端傳到 server端的 query body。
Summary
Apollo cahce 的設計博大而精深,也在最新的 ApolloClient 3.0 中引進了不少新的操作,實在無法用一篇文章去解釋全部的操作與細節。總而言之,ApolloClient 的 InMemoryCahce
是為前端的 graphql 而設計,ApolloServer 端的是針對發送給不同 Data Service 的 response 做 cache。
參考資料: