O que há de errado com o GraphQL

Recentemente, o GraphQL está ganhando cada vez mais popularidade. Sintaxe graciosa de solicitações, tipificação e assinaturas.


Parece: "aqui está - nós encontramos a linguagem ideal para a troca de dados!" ...


Estou desenvolvendo esse idioma há mais de um ano e digo: tudo está longe de ser tranquilo. O GraphQL tem momentos desconfortáveis ​​e problemas realmente fundamentais no próprio design da linguagem.


Por outro lado, a maioria dessas "mudanças de design" foram feitas por uma razão - isso ocorreu devido a várias considerações. De fato, o GraphQL não é para todos e pode não ser a ferramenta que você precisa. Mas as primeiras coisas primeiro.


Eu acho que vale a pena fazer uma pequena observação sobre onde eu uso essa linguagem. Este é um painel de administração do SPA bastante complicado, a maioria das operações nas quais são CRUDs não triviais (entidades complexas). Uma parte significativa da argumentação deste material está relacionada à natureza do aplicativo e à natureza dos dados processados. Em aplicativos de um tipo diferente (ou com uma natureza diferente dos dados), esses problemas podem não surgir em princípio.

1. NON_NULL


Este não é um problema sério. Em vez disso, essa é uma série de inconvenientes relacionados à organização do trabalho com anulável no GraphQL.


Existem linguagens de programação funcionais (e não apenas), esse paradigma é mônadas. Portanto, existe uma mônada Maybe (Haskel) ou Option (Scala) A conclusão é que o valor contido nessa mônada pode ou não existir (ou seja, ser nulo). Bem, ou pode ser implementado através de enum, como no Rust.


De uma forma ou de outra, e na maioria dos idiomas, esse valor, que "envolve" o original, torna nula uma opção adicional à principal. E sintaticamente - é sempre uma adição ao tipo principal. Isso nem sempre é apenas uma classe de tipo separada - em alguns idiomas, é apenas uma adição na forma de sufixo ou prefixo ? .


No GraqhQL, o oposto é verdadeiro. Todos os tipos são anuláveis ​​por padrão - e isso não está apenas marcando o tipo como anulável, é a mônada Maybe , pelo contrário.


E se considerarmos a seção de introspecção do campo de name para esse esquema:


 #       schema -  ,    schema { query: Query } type Query { #       NonNull name: String! } 

nós encontramos:


imagem


Tipo de String NON_NULL em NON_NULL


1.1 SAÍDA


Porque assim? Em resumo, ele está conectado ao design de linguagem "tolerante" padrão (entre outras coisas, é amigável à arquitetura de microsserviço).


Para entender a essência dessa "tolerância", considere um exemplo um pouco mais complexo, no qual todos os valores retornados são estritamente agrupados em NON_NULL:


 type User { name: String! #  :       . friends: [User!]! } type Query { #  :       . users(ids: [ID!]!): [User!]! } 

Suponha que tenhamos um serviço que retorne uma lista de usuários e uma "amizade" separada de microsserviço que nos retorne uma correspondência para os amigos do usuário. Então, em caso de falha do serviço de "amizade", não poderemos listar os usuários. Precisa corrigir a situação:


 type User { name: String! #    -  null   . #    ""  -      ,    . friends: [User!] } 

Essa é a tolerância a erros internos. Um exemplo, é claro, exagerado. Mas espero que você tenha entendido a essência.


Além disso, você pode facilitar sua vida em outras situações. Suponha que haja usuários remotos e os identificadores de amigos possam ser armazenados em alguma estrutura externa não relacionada. Poderíamos simplesmente eliminar e devolver apenas o que possuímos, mas não conseguiremos entender o que exatamente foi eliminado.


 type Query { #  null   . #                . users(ids: [ID!]!): [User]! } 

Está tudo bem. E qual é o problema?
Em geral, não é um problema muito grande - então o gosto. Mas se você tem um aplicativo monolítico com um banco de dados relacional, os erros mais prováveis ​​são realmente erros, e a API deve ser o mais rigorosa possível. Olá, pontos de exclamação! Onde você puder.


Eu gostaria de poder "inverter" esse comportamento e colocar pontos de interrogação em vez de pontos de exclamação) Seria mais familiar de alguma forma.


1.2 ENTRADA


Mas ao entrar, anulável é uma história completamente diferente. Este é um batente do nível da caixa de seleção em HTML (acho que todo mundo se lembra dessa não-obviedade quando o campo de uma caixa de seleção desmarcada simplesmente não é enviado para trás).


Considere um exemplo:


 type Post { id: ID! title: String! #  :     null description: String content: String! } input PostInput { title: String! #  :     ,   description: String content: String! } type Mutation { createPost(post: PostInput!): Post! } 

Até agora, tudo bem. Adicionar atualização:


 type Mutation { createPost(post: PostInput!): Post! updatePost(id: ID!, post: PostInput!): Post! } 

E agora a pergunta é: o que podemos esperar do campo de descrição ao atualizar a publicação? O campo pode ser nulo ou pode estar ausente por completo.


Se o campo estiver faltando, o que precisa ser feito? Não atualizá-lo? Ou defina-o como nulo? A linha inferior é que permitir nulo e permitir a ausência de um campo são duas coisas diferentes. No entanto, o GraphQL é a mesma coisa.


2. Separação de entrada e saída


Isso é apenas dor. No modelo de trabalho CRUD, você obtém o objeto de trás "torce" e envia de volta. Grosso modo, esse é o mesmo objeto. Mas você só precisa descrevê-lo duas vezes - para entrada e saída. E nada pode ser feito com isso, exceto para escrever um gerador de código para esse negócio. Eu preferiria separar na "entrada e saída" não os objetos em si, mas os campos do objeto. Por exemplo, modificadores:


 type Post { input output text: String! output updatedAt(format: DateFormat = W3C): Date! } 

ou usando diretivas:


 type Post { text: String! @input @output updatedAt(format: DateFormat = W3C): Date! @output } 

3. Polimorfismo


Os problemas de separação de tipos em entrada e saída não se limitam a uma descrição dupla. Ao mesmo tempo, para tipos genéricos, você pode definir interfaces generalizadas:


 interface Commentable { comments: [Comment!]! } type Post implements Commentable { text: String! comments: [Comment!]! } type Photo implements Commentable { src: URL! comments: [Comment!]! } 

ou sindicatos


 type Person { firstName: String, lastName: String, } type Organiation { title: String } union Subject = Organiation | Person type Account { login: String subject: Subject } 

Você não pode fazer o mesmo para tipos de entrada. Existem vários pré-requisitos para isso, mas isso se deve em parte ao fato de o json ser usado como formato de dados para transporte. No entanto, na saída, o campo __typename é usado para especificar o tipo. Por que era impossível fazer o mesmo ao entrar - não é muito claro. Parece-me que esse problema poderia ser resolvido de maneira um pouco mais elegante abandonando o json durante o transporte e inserindo seu próprio formato. Algo no espírito:


 union Subject = OrganiationInput | PersonInput input AccountInput { login: String! password: String! subject: Subject! } 

 #     { account: AccountInput { login: "Acme", password: "***", subject: OrganiationInput { title: "Acme Inc" } } } 

 #      { account: AccountInput { login: "Acme", password: "***", subject: PersonInput { firstName: "Vasya", lastName: "Pupkin", } } } 

Mas isso tornaria necessário escrever analisadores adicionais para esse negócio.


4. Genéricos


O que há de errado com o GraphQL com genéricos? E tudo é simples - eles não são. Vamos fazer uma consulta típica do índice CRUD com uma paginação ou cursor - isso não é importante. Vou dar um exemplo com paginação.


 input Pagination { page: UInt, perPage: UInt, } type Query { users(pagination: Pagination): PageOfUsers! } type PageOfUsers { total: UInt items: [User!]! } 

e agora para organizações


 type Query { organizations(pagination: Pagination): PageOfOrganizations! } type PageOfOrganizations { total: UInt items: [Organization!]! } 

e assim por diante ... como eu gostaria de ter genéricos para isso


 type PageOf<T> { total: UInt items: [T!]! } 

então eu escrevia


 type Query { users(page: UInt, perPage: UInt): PageOf<User>! } 

Sim toneladas de aplicações! Devo falar sobre os genéricos?


5. Namespaces


Eles também não estão lá. Quando o número de tipos no sistema é superior a cem e meia, a probabilidade de colisões de nomes tende a cem por cento.


E todos os tipos de Service_GuideNDriving_Standard_Model_Input aparecem. Não estou falando de namespaces completos em diferentes pontos de extremidade, como no SOAP (sim, sim, é terrível, mas os namespaces são criados lá perfeitamente). E pelo menos vários esquemas em um ponto de extremidade com a capacidade de "atrapalhar" tipos entre esquemas.


Total


O GraphQL é uma boa ferramenta. Ele se encaixa perfeitamente em uma arquitetura tolerante de microsserviço, orientada, antes de tudo, para a saída de informações e para uma entrada determinística simples.


Se você tiver entidades polimórficas para entrar, poderá ter problemas.
A separação dos tipos de entrada e saída, bem como a falta de genéricos - geram um monte de rabiscos do zero.


Graphql não é realmente (e às vezes nem um pouco ) sobre CRUD.


Mas isso não significa que você não pode comer :)


No próximo artigo, quero falar sobre como luto (e às vezes com sucesso) com alguns dos problemas descritos acima.

Source: https://habr.com/ru/post/pt425041/


All Articles