GraphQL e cache: duas palavras que não combinam muito bem.
A razão é que GraphQL opera via POST
executando todas as consultas em um único endpoint e passando parâmetros pelo corpo da solicitação. O URL desse único endpoint produzirá respostas diferentes, o que significa que não pode ser armazenado em cache-pelo menos não usando o URL como identificador.
“Mas espere um segundo”, você diz. “GraphQL certamente tem cache, certo?”
Sim, fazendo isso no cliente por meio do Apollo Client e bibliotecas semelhantes, que armazenam em cache os objetos retornados independentemente de cada um outro, identificando-os por seu ID global exclusivo.
Mas isso é um hack. Esta solução só existe porque GraphQL não consegue lidar com o cache no servidor, para o qual normalmente usamos o URL como o
O armazenamento em cache no cliente tem algumas desvantagens:
- O aplicativo tem mais JavaScript para rodar no lado do cliente. Acessar o site por meio de um celular de última geração terá um impacto no desempenho
- O aplicativo ficou mais complexo e com mais partes móveis, pois agora também precisamos nos preocupar com a implementação da camada de cache
- Nem todo mundo entende JavaScript (por exemplo, o site pode ser codificado em PHP), mas agora lidar com JS também se torna uma responsabilidade
Então, qual é a solução?
É, simplesmente, usar os padrões. Nesse caso, o padrão é Cache HTTP .
“Sim, mas esse é o ponto-não podemos usar o cache HTTP! Ou do que estamos falando? ”
Certo. Mas, sabendo que queremos usar o cache HTTP, podemos abordar o problema de um ângulo diferente. Em vez de perguntar: “Como podemos armazenar em cache o GraphQL?” podemos perguntar: “Para usar o cache HTTP, como devemos usar o GraphQL?”
Neste artigo, vamos responder a esta pergunta.
Acessando GraphQL via GET
Usar o cache HTTP significa que vamos armazenar em cache a resposta GraphQL usando o URL como identificador. Isso tem duas implicações:
- Devemos acessar o único endpoint do GraphQL via
GET
- Devemos passar a consulta e as variáveis como parâmetros de URL
Então, se o único endpoint for /graphql
, a operação GET
pode ser executada no URL /graphql?query=...&variables=...
.
Isso se aplica à recuperação de dados do servidor (por meio da operação query
). Para dados mutantes (por meio da operação mutation
), ainda devemos usar POST
. Não há nenhum problema aqui, já que as mutações são sempre executadas de novo; não podemos armazenar em cache os resultados de uma mutação, portanto, não usaríamos o cache HTTP com isso de qualquer maneira.
Essa abordagem funciona (e é até sugerida no site oficial ), mas existe são certas considerações que devemos ter em mente.
Codificando consultas GraphQL via parâmetro de URL
Uma consulta GraphQL normalmente abrange várias linhas. Por exemplo:
{ Postagens { eu ia título } }
No entanto, não podemos inserir essa string de várias linhas diretamente no parâmetro de URL.
A solução é codificá-lo. Por exemplo, o cliente GraphiQL codificará a consulta acima assim :
% 7B% 0A% 20% 20posts% 20% 7B% 0A% 20% 20% 20% 20id% 0A% 20% 20% 20% 20% 20title% 0A% 20% 20% 7D% 0A % 7D
Tudo bem, isso funciona. Mas não parece muito bom, certo? Quem pode entender essa consulta?
Uma das virtudes do GraphQL é que suas consultas são muito fáceis de entender. Com alguma prática, uma vez que vemos a consulta, a entendemos imediatamente. Mas uma vez que foi codificado, tudo se foi, e apenas as máquinas podem compreendê-lo; o humano está fora da equação.
Outra solução poderia ser substituir todas as novas linhas na consulta por um espaço, o que funciona porque novas linhas não adicionam significado semântico para a consulta . Em seguida, a consulta acima pode ser representado como:
? query={posts {id title}}
Isso funciona bem para consultas simples. Mas se você tiver uma consulta muito longa, abrindo e fechando muitas chaves e adicionando argumentos de campo e diretivas, torna-se cada vez mais difícil de entender.
Por exemplo, esta consulta :
{ postagens (limite: 5) { eu ia title @titleCase excerto @default ( valor:"Sem título", condição: IS_EMPTY ) autor { nome } Tag { eu ia nome } comentários( limite: 3, pedido:"data | DESC" ) { eu ia data (formato:"d/m/Y") autor { nome } contente } } }
Tornaria-se esta consulta de linha única:
{posts (limite: 5) {id title @titleCase excerpt @default (valor:"Sem título", condição: IS_EMPTY) autor {name} tags {id name} comentários (limite: 3, pedido:"data | DESC") {id data (formato:"d/m/Y") autor {nome} conteúdo}}}
Mais uma vez, funciona , mas não saberemos o que estamos executando. E se a consulta também contém fragmentos, então esqueça-o totalmente-não há como entendermos isso.
Então, o que podemos fazer a respeito?
GraphQL sobre HTTP
Em primeiro lugar, as boas notícias: as partes interessadas da comunidade GraphQL identificaram este problema e começaram a trabalhar no GraphQL sobre HTTP especificação, que padronizará como todos (servidores GraphQL, clientes, bibliotecas, etc.) comunicarão suas consultas GraphQL por meio do parâmetro de URL.
Em segundo lugar, as notícias não tão boas: o progresso neste esforço parece ser lento e a especificação até agora não é abrangente o suficiente para ser utilizável. Portanto, ou esperamos por um período de tempo incerto ou procuramos outra solução.
Consultas persistentes para o resgate
Se passar a consulta no URL não for satisfatório, que outra opção temos? Bem, para não passar a consulta na URL!
Essa abordagem é chamada de “consulta persistente”. Armazenamos a consulta no servidor e usamos um identificador (como um ID numérico ou uma string exclusiva produzida pela aplicação de um algoritmo de hash com a consulta como entrada) para recuperá-la. Por fim, passamos esse identificador como o parâmetro de URL em vez da consulta.
Por exemplo, a consulta pode ser identificada com o ID 2908
(ou um hash como "50ac3e81"
), e então executamos o GET
operação em relação ao URL /graphql? id=2908
. O servidor GraphQL irá então recuperar a consulta correspondente a este ID, executá-la e retornar os resultados.
Usando consultas persistentes, a implementação do cache HTTP torna-se um problema.
Problema resolvido! Se você deseja usar o cache HTTP em seu servidor GraphQL, encontre um servidor GraphQL que ofereça suporte a consultas persistentes, seja nativamente ou por meio de alguma biblioteca.
Calculando o valor max-age
Vamos para o próximo desafio!
O cache HTTP funciona enviando o cabeçalho Cache-Control
na resposta, com um valor max-age
indicando a quantidade de tempo que a resposta deve ser armazenada em cache ou no-store
indicando não armazená-lo em cache.
Como o servidor GraphQL calculará o valor max-age
da consulta, considerando que campos diferentes podem ter valores max-age
diferentes?
A resposta é obter o valor max-age
para todos os campos solicitados na consulta e descobrir qual é o mais baixo. Essa será a max-age
da resposta.
Por exemplo, digamos que temos uma entidade do tipo Usuário
. Seguindo o comportamento atribuído a esta entidade, podemos atribuir por quanto tempo o campo correspondente deve ser armazenado em cache:
Seu ID nunca mudará ⇒ Damos ao campo
id
uma idade máxima
de 1 ano
Seu URL será atualizado de forma muito aleatória (se for o caso) ⇒ Damos ao campo
url
uma idade máxima
de 1 dia
O nome da pessoa pode mudar de vez em quando (por exemplo, para adicionar um status, ou para dizer “Milton (usa uma máscara)”) ⇒ Damos ao campo
nome
uma idade máxima
de 1 hora
O carma do usuário no site pode mudar a qualquer momento (por exemplo, após alguém votar positivamente em seu comentário) ⇒ Damos ao campo
karma
uma idade máxima
de 1 minuto
Se estiver consultando os dados do usuário conectado, a resposta não pode ser armazenada em cache (independentemente do campo que estamos buscando) ⇒ A
max-age
deve ser não armazenar
Como resultado, a resposta às seguintes consultas GraphQL terá os seguintes valores max-age
(neste exemplo, ignoramos max-age
para o campo Root.users
, mas na prática, também será levado em consideração):
Consulta | max-age value |
---|---|
{ Comercial { eu ia } } |
1 ano |
{ Comercial { eu ia url } } |
1 dia |
{ Comercial { eu ia url nome } } |
1 hora |
{ Comercial { eu ia url nome carma } } |
1 minuto |
{ Eu { eu ia url nome carma } } |
não armazenar (não armazenar em cache) |
Adicionando diretivas para calcular o valor max-age
Como o servidor GraphQL pode calcular o valor max-age
da resposta? Como esse valor dependerá de todos os campos presentes na consulta, há um candidato óbvio para fazer isso: diretivas.
Uma diretiva de tipo de esquema pode ser atribuída a um campo e podemos personalizar sua configuração por meio de argumentos de diretiva.
Portanto, podemos criar uma diretiva @cacheControl
com o argumento maxAge
do tipo Int
(medindo segundos). Especificar maxAge
com valor 0
é equivalente a no-store
. Se não for fornecido (o argumento foi definido como não obrigatório), um padrão predefinido max-age
é usado.
Agora podemos configurar nosso esquema para satisfazer a max-age
definida para todos os campos anteriormente. Usando a linguagem de definição de esquema (SDL), terá a seguinte aparência:
diretiva @cacheControl (maxAge: Int) em FIELD_DEFINITION tipo User { id: ID @cacheControl (maxAge: 31557600) url: URL @cacheControl (maxAge: 86400) nome: String @cacheControl (maxAge: 3600) karma: Int @cacheControl (maxAge: 60) } tipo Root { eu: Usuário @cacheControl (maxAge: 0) }
Codificando a diretiva @cacheControl
Vou demonstrar minha implementação da diretiva @cacheControl
para o servidor GraphQL API para WordPress , que é codificado em PHP. (Este servidor tem consultas persistentes nativas e Cache HTTP .)
A resolução da diretiva é muito simples: ele apenas pega o valor maxAge
do argumento da diretiva e o injeta em um serviço chamado CacheControlEngine
:
public function resolveDirective (): void { $ maxAge=$ this-> DirectiveArgsForSchema ['maxAge']; if (! is_null ($ maxAge)) { $ this-> cacheControlEngine-> addMaxAge ($ maxAge); } }
Sempre que injetar um novo valor max-age
, o serviço CacheControlEngine
computar o valor inferior e armazená-lo em seu estado:
classe CacheControlEngine { protegido? int $ minimumMaxAge=null; public function addMaxAge (int $ maxAge): void { if (is_null ($ this-> minimumMaxAge) || $ maxAge <$ this-> minimumMaxAge) { $ this-> minimumMaxAge=$ maxAge; } } }
O serviço pode então gerar o cabeçalho Cache-control
, com o valor max-age
para a resposta:
classe CacheControlEngine { public function getCacheControlHeader ():? string { if (! is_null ($ this-> minimumMaxAge)) { //Minimum max-age=0=> `no-store` if ($ this-> minimumMaxAge===0) { retornar'Cache-Control: no-store'; } return sprintf ( 'Cache-Control: max-age=% s', $ this-> minimumMaxAge ); } return null; } }
Finalmente, o servidor GraphQL obterá o cabeçalho Cache-Control
do serviço e o adicionará à resposta.
Conclusão
No argumento interminável de se GraphQL é melhor que REST (e vice-versa), REST sempre teve um ás na manga: cache do lado do servidor.
Mas também podemos ter GraphQL compatível com cache HTTP. Basta armazenar a consulta no servidor e, em seguida, acessar essa “consulta persistente” por meio de GET
, fornecendo o ID da consulta como um parâmetro de URL. É uma troca mais do que justificada e mais do que vale a pena.
GraphQL e cache: duas palavras que combinam muito bem.
A postagem Cache HTTP no GraphQL apareceu primeiro em LogRocket Blog .