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.
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:
- Tenho um registro nas costas.
- Campos de registro editados.
- 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
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.