[O que há de errado com o GraphQL] ... E como lidar com isso

No artigo anterior , examinamos pontos inconvenientes no sistema de tipos GraphQL.
E agora vamos tentar derrotar alguns deles. Todos os interessados, por favor, sob o gato.


A numeração da partição corresponde aos problemas com os quais eu consegui lidar.


1.2 ENTRADA NON_NULL


Neste ponto, examinamos a ambiguidade que gera um recurso de implementação anulável no GraphQL.


E o problema é que ele não permite implementar o conceito de atualização parcial do zero - um análogo do método HTTP PATCH na arquitetura REST. Nos comentários sobre material passado, fui fortemente criticado pelo pensamento "REST". Só posso dizer que a arquitetura CRUD me obriga a isso. E eu não estava pronto para desistir dos benefícios do REST, simplesmente porque "não faça isso". Sim, e uma solução para este problema foi encontrada.


E assim, de volta ao problema. Como todos sabemos, o script CRUD ao atualizar o registro se parece com o seguinte:


  1. Tenho um registro nas costas.
  2. Campos de registro editados.
  3. Enviou um registro para trás.

O conceito de atualização parcial, neste caso, deve permitir o envio apenas dos campos que foram alterados.
Então, se definirmos um modelo de entrada dessa maneira


 input ExampleInput { foo: String! bar: String } 

ao mapear uma variável do tipo ExampleInput com este valor


 { "foo": "bla-bla-bla" } 

em um DTO com esta estrutura:


 ExampleDTO { foo: String #   bar: ?String #   } 

nós obtemos um objeto DTO com este valor:


 { foo: "bla-bla-bla", bar: null } 

e ao mapear uma variável com esse valor


 { "foo": "bla-bla-bla", "bar": null } 

obtemos um objeto DTO com o mesmo valor da última vez:


 { foo: "bla-bla-bla", bar: null } 

Ou seja, ocorre entropia - perdemos informações sobre se o campo foi transmitido do cliente ou não.
Nesse caso, não está claro o que precisa ser feito com o campo do objeto final: não toque nele porque o cliente não passou o campo ou defina-o como null porque o cliente passou null .


A rigor, o GraphQL é um protocolo RPC. E comecei a pensar em como faço essas coisas nas costas e em quais procedimentos devo chamar para fazer exatamente da maneira que quero. E no back-end, faço uma atualização parcial dos campos como este:


 $repository->find(42)->setFoo('bla-bla-lba'); 

Ou seja, eu literalmente não toco no setter de uma propriedade da entidade, a menos que precise alterar o valor dessa propriedade. Se você mudar isso para o esquema GraphQL, obterá o seguinte resultado:


 type Mutation { entityRepository: EntityManager! } type EntityManager { update(id: ID!): PersitedEntity } type PersitedEntity { setFoo(foo: String!): String! setBar(foo: String): String } 

Agora, se quisermos, podemos chamar o método setBar e definir seu valor como nulo, ou não tocar nesse método, e o valor não será alterado. Assim, uma boa implementação de partial update é lançada. Não é pior que PATCH do famoso REST.


Nos comentários de material passado, summerwind perguntou: por que precisamos de uma partial update ? Eu respondo: existem MUITO grandes campos.

3. Polimorfismo


Muitas vezes acontece que você precisa enviar para as entidades de entrada que são "iguais e iguais", mas não completamente. Vou usar o exemplo da criação de uma conta a partir de material passado.


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

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

Obviamente, não podemos enviar dados com essa estrutura para um argumento - o GraphQL simplesmente não nos permite fazer isso. Então, você precisa resolver de alguma forma esse problema.


Método 0 - testa


A primeira coisa que vem à mente é a separação da parte variável da entrada:


 input AccountInput { login: String! password: Password! subjectOrganization: OrganiationInput subjectPerson: PersonInput } 

Hmm ... quando vejo esse código, muitas vezes me lembro de Josephine Pavlovna. Isso não combina comigo.


Método 1 - não na testa, mas na testa
Em seguida, soube que, para identificar entidades, uso o UUID (geralmente recomendo a todos - isso ajudará mais de uma vez). E isso significa que eu posso criar entidades válidas diretamente no cliente, vinculá-las pelo identificador e enviá-las para o back-end, separadamente.


Então podemos fazer algo no espírito de:


 input AccountInput { login: String! password: Password! subject: SubjectSelectInput! } input SubjectSelectInput { id: ID! } type Mutation { createAccount( organization: OrganizationInput, person: PersonInput, account: AccountInput! ): Account! } 

ou, que acabou sendo ainda mais conveniente (por que é mais conveniente, vou lhe dizer quando chegarmos à geração de interfaces de usuário), divida-o em diferentes métodos:


 type Mutation { createAccount(account: AccountInput!): Account! createOrganization(organization: OrganizationInput!): Organization! createPerson(person: PersonInput!) : Person! } 

Em seguida, precisaremos enviar uma solicitação para createAccount e createOrganization / createPerson
um lote. Vale ressaltar que o processamento do lote deve ser agrupado em uma transação.


Método 2 - o escalar mágico
O truque é que o escalar no GraphQL não é apenas Int , Sting , Float , etc. Isso geralmente é tudo (bem, enquanto o JSON pode lidar com isso, é claro).
Então podemos simplesmente declarar um escalar:


 scalar SubjectInput 

Em seguida, escreva seu manipulador nele e ele não ficará no vapor. Em seguida, podemos deslizar facilmente os campos variáveis ​​para a entrada.


Qual o caminho a escolher? Eu uso os dois e desenvolvi uma regra para mim:
Se a entidade pai for uma Raiz Agregada para o filho, escolho o segundo método, caso contrário, o primeiro.


4. Genéricos.


Tudo é trivial aqui e eu não criei nada melhor do que a geração de código. E sem o Rails (pacote railt / sdl) eu não teria conseguido (ou melhor, teria feito o mesmo com muletas). O truque é que o Rail permite definir diretivas em nível de documento (não existe essa posição para diretivas na folha de especificações).


 directive @example on DOCUMENT 

Ou seja, as diretivas não estão anexadas a nada além do documento em que são chamadas.


Eu introduzi as seguintes diretrizes:


 directive @defineMacro(name: String!, template: String!) on DOCUMENT directive @macro(name: String!, arguments: [String]) on DOCUMENT 

Eu acho que ninguém precisa explicar a essência das macros ...


Por enquanto é tudo. Não acho que esse material cause tanto ruído quanto o passado. Mesmo assim, o título era bastante "amarelo")


Nos comentários do artigo anterior, os residentes de Khabrovsk se afogaram por compartilhar o acesso ... então o próximo artigo será sobre autorização.

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


All Articles