[¿Qué hay de malo con GraphQL] ... y cómo lidiar con eso

En el artículo anterior , examinamos puntos inconvenientes en el sistema de tipo GraphQL.
Y ahora intentaremos derrotar a algunos de ellos. Todos los interesados, por favor, bajo cat.


La numeración de particiones corresponde a los problemas que pude resolver.


1.2 ENTRADA NO NULA


En este punto, examinamos la ambigüedad que genera una característica de implementación anulable en GraphQL.


Y el problema es que no permite implementar el concepto de actualización parcial desde cero, un análogo del método HTTP PATCH en la arquitectura REST. En los comentarios sobre material pasado, me criticaron mucho por el pensamiento "REST". Solo puedo decir que la arquitectura CRUD me obliga a esto. Y no estaba listo para renunciar a los beneficios de REST, simplemente porque "no hagas esto". Sí, y se encontró una solución a este problema.


Y así, volviendo al problema. Como todos sabemos, el script CRUD al actualizar el registro se ve así:


  1. Tengo un registro de la parte de atrás.
  2. Campos de registro editados.
  3. Envió un registro a la parte de atrás.

El concepto de actualización parcial, en este caso, debería permitirnos enviar solo aquellos campos que han sido modificados.
Entonces, si definimos un modelo de entrada de esta manera


 input ExampleInput { foo: String! bar: String } 

luego, al mapear una variable de tipo ExampleInput con este valor


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

en un DTO con esta estructura:


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

obtenemos un objeto DTO con este valor:


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

y al mapear una variable con este valor


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

obtenemos un objeto DTO con el mismo valor que la última vez:


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

Es decir, se produce entropía: perdemos información sobre si el campo se transmitió desde el cliente o no.
En este caso, no está claro qué debe hacerse con el campo del objeto final: no lo toque porque el cliente no pasó el campo ni lo establezca como null porque el cliente pasó a null .


Estrictamente hablando, GraphQL es un protocolo RPC. Y comencé a pensar en cómo hago esas cosas en la parte posterior y qué procedimientos debo llamar para hacer exactamente lo que quiero. Y en el backend, hago una actualización parcial de los campos como este:


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

Es decir, literalmente no toco el establecedor de una propiedad de entidad, a menos que necesite cambiar el valor de esta propiedad. Si cambia esto al esquema GraphQL, obtendrá el siguiente resultado:


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

Ahora, si queremos, podemos llamar al método setBar y establecer su valor en nulo, o no tocar este método, y luego el valor no se cambiará. Por lo tanto, sale una buena implementación de partial update . No peor que PATCH del notorio RESTO.


En comentarios sobre material pasado, summerwind preguntó: ¿por qué necesitamos una partial update ? Respondo: hay campos MUY grandes.

3. Polimorfismo


A menudo sucede que necesita enviar a las entidades de entrada que son una especie de "uno y el mismo" pero no del todo. Usaré el ejemplo de crear una cuenta a partir de material pasado.


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

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

Obviamente, no podemos enviar datos con tal estructura para un argumento: GraphQL simplemente no nos permitirá hacer esto. Entonces, de alguna manera necesitas resolver este problema.


Método 0 - frente


Lo primero que viene a la mente es la separación de la parte variable de la entrada:


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

Hmm ... cuando veo ese código, a menudo recuerdo a Josephine Pavlovna. No me queda bien.


Método 1: no en la frente, sino en la frente
Luego, el hecho me ayudó a identificar entidades, utilizo UUID (generalmente lo recomiendo a todos, ayudará más de una vez). Y esto significa que puedo crear entidades válidas directamente en el cliente, unirlas por identificador y enviarlas al back-end, por separado.


Entonces podemos hacer algo en el espíritu de:


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

o, lo que resultó ser aún más conveniente (por qué es más conveniente, le diré cuando lleguemos a la generación de interfaces de usuario), divídalo en diferentes métodos:


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

Luego, tendremos que enviar una solicitud para crear Cuenta y crear Organización / crear Persona
un lote Vale la pena señalar que el procesamiento del lote debe estar envuelto en una transacción.


Método 2 - el escalar mágico
El truco es que el escalar en GraphQL no es solo Int , Sting , Float , etc. Esto generalmente es cualquier cosa (bueno, mientras que JSON puede manejar esto, por supuesto).
Entonces podemos simplemente declarar un escalar:


 scalar SubjectInput 

Luego, escriba su controlador en él, y no estará al vapor. Entonces podemos deslizar fácilmente campos variables a la entrada.


¿Qué forma de elegir? Yo uso ambos, y he desarrollado una regla para mí:
Si la entidad principal es una raíz agregada para el elemento secundario, entonces elijo el segundo método, de lo contrario, el primero.


4. Genéricos.


Aquí todo es trivial y nada mejor que la generación de código que se me ocurrió. Y sin Rails (paquete railt / sdl) no habría podido (o más bien, habría hecho lo mismo con muletas). El truco es que Rail le permite definir directivas a nivel de documento (no existe tal posición para las directivas en la hoja de especificaciones).


 directive @example on DOCUMENT 

Es decir, las directivas no se adjuntan a otra cosa que no sea el documento en el que se llaman.


Introduje las siguientes directivas:


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

Creo que nadie necesita explicar la esencia de las macros ...


Eso es todo por ahora. No creo que este material cause tanto ruido como el pasado. De todos modos, el título allí era bastante "amarillo")


En los comentarios al artículo anterior, los residentes de Khabrovsk se ahogaron por compartir el acceso ... por lo que el próximo artículo tratará sobre la autorización.

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


All Articles