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 ​​todos os dados de resposta em conjunto

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:

  1. Devemos acessar o único endpoint do GraphQL via GET
  2. 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 .

Source link