Recientemente, GraphQL está ganando más y más popularidad. Agradable sintaxis de solicitudes, tipificación y suscripciones.
Parece: "aquí está, ¡hemos encontrado el lenguaje ideal para el intercambio de datos!" ...
Llevo más de un año desarrollando este lenguaje y les diré: todo está lejos de ser tan sencillo. GraphQL tiene momentos incómodos y problemas realmente fundamentales en el diseño del lenguaje en sí.
Por otro lado, la mayoría de estos "movimientos de diseño" se hicieron por una razón: esto se debió a varias consideraciones. De hecho, GraphQL no es para todos, y puede que no sea la herramienta que necesita en absoluto. Pero lo primero es lo primero.
Creo que vale la pena hacer un pequeño comentario sobre dónde uso este idioma. Este es un panel de administración de SPA bastante complicado, la mayoría de las operaciones son CRUD (entidades complejas) bastante triviales. Una parte importante de la argumentación en este material está relacionada con la naturaleza de la aplicación y la naturaleza de los datos procesados. En aplicaciones de un tipo diferente (o con una naturaleza diferente de los datos), tales problemas pueden no surgir en principio.
1. NON_NULL
Este no es un problema serio. Más bien, esta es una serie completa de inconvenientes relacionados con la forma en que se organiza el trabajo con nulos en GraphQL.
Hay lenguajes de programación funcionales (y no solo), tal paradigma es mónadas. Entonces, existe una mónada tal Maybe
(Haskel) u Option
(Scala). La conclusión es que el valor contenido dentro de una mónada puede existir o no (es decir, ser nulo). Bueno, o puede implementarse a través de enum, como en Rust.
De una forma u otra, y en la mayoría de los idiomas, este valor, que "envuelve" el original, hace que nulo sea una opción adicional al principal. Y sintácticamente, siempre es una adición al tipo principal. Esto no siempre es solo una clase de tipo separada; en algunos idiomas, ¿es solo una adición en forma de sufijo o prefijo ?
.
En GraqhQL, lo contrario es cierto. Todos los tipos son anulables por defecto, y esto no es solo marcar el tipo como anulable, es la mónada Maybe
, por el contrario.
Y si consideramos la sección de introspección del campo de name
para dicho esquema:
encontramos:

Tipo de String
envuelta en NON_NULL
1.1. SALIDA
Por qué En resumen, está conectado con el diseño de lenguaje "tolerante" predeterminado (entre otras cosas, es amigable con la arquitectura de microservicios).
Para comprender la esencia de esta "tolerancia", considere un ejemplo un poco más complejo, en el que todos los valores devueltos están estrictamente envueltos en NON_NULL:
type User { name: String!
Supongamos que tenemos un servicio que devuelve una lista de usuarios y una "amistad" de microservicios separada que nos devuelve una coincidencia para los amigos del usuario. Luego, en caso de falla del servicio de "amistad", no podremos enumerar a los usuarios en absoluto. Necesito arreglar la situación:
type User { name: String!
Esta es la tolerancia a los errores internos. Un ejemplo, por supuesto, descabellado. Pero espero que hayas captado la esencia.
Además, puede hacer su vida un poco más fácil en otras situaciones. Supongamos que hay usuarios remotos, y los identificadores de amigos pueden almacenarse en alguna estructura externa no relacionada. Podríamos eliminar y devolver solo lo que tenemos, pero luego no podremos entender qué se eliminó exactamente.
type Query {
Todo esta bien ¿Y cuál es el problema?
En general, no es un gran problema, por lo que el sabor. Pero si tiene una aplicación monolítica con una base de datos relacional, los errores más probables son realmente errores, y la API debe ser lo más estricta posible. ¡Hola, signos de exclamación! Donde sea que puedas.
Me gustaría poder "invertir" este comportamiento y colocar signos de interrogación en lugar de signos de exclamación) Sería más familiar de alguna manera.
Pero al entrar, anulable es una historia completamente diferente. Este es un marco del nivel de la casilla de verificación en HTML (creo que todos recuerdan esta falta de evidencia cuando el campo de una casilla de verificación sin marcar simplemente no se envía al reverso).
Considere un ejemplo:
type Post { id: ID! title: String!
Hasta ahora todo bien. Agregar actualización:
type Mutation { createPost(post: PostInput!): Post! updatePost(id: ID!, post: PostInput!): Post! }
Y ahora la pregunta es: ¿qué podemos esperar del campo de descripción al actualizar la publicación? El campo puede ser nulo o puede estar ausente por completo.
Si falta el campo, ¿qué hay que hacer? ¿No lo actualizas? ¿O ponerlo a nulo? La conclusión es que permitir nulo y permitir la ausencia de un campo son dos cosas diferentes. Sin embargo, GraphQL es lo mismo.
2. Separación de entrada y salida.
Esto es solo dolor. En el modelo de trabajo CRUD, obtiene el objeto de la parte posterior "gírelo" y lo envía de vuelta. En términos generales, este es uno y el mismo objeto. Pero solo tiene que describirlo dos veces, para entrada y salida. Y no se puede hacer nada con esto, excepto escribir un generador de código para este negocio. Preferiría separar en la "entrada y salida" no los objetos en sí, sino los campos del objeto. Por ejemplo, modificadores:
type Post { input output text: String! output updatedAt(format: DateFormat = W3C): Date! }
o usando directivas:
type Post { text: String! @input @output updatedAt(format: DateFormat = W3C): Date! @output }
3. Polimorfismo
Los problemas de separar los tipos en entrada y salida no se limitan a una doble descripción. Al mismo tiempo, para los tipos genéricos puede definir interfaces generalizadas:
interface Commentable { comments: [Comment!]! } type Post implements Commentable { text: String! comments: [Comment!]! } type Photo implements Commentable { src: URL! comments: [Comment!]! }
o sindicatos
type Person { firstName: String, lastName: String, } type Organiation { title: String } union Subject = Organiation | Person type Account { login: String subject: Subject }
No puede hacer lo mismo para los tipos de entrada. Hay una serie de requisitos previos para esto, pero esto se debe en parte al hecho de que json se utiliza como formato de datos para el transporte. Sin embargo, en la salida, el campo __typename
se usa para especificar el tipo. Por qué era imposible hacer lo mismo al entrar, no está muy claro. Me parece que este problema podría resolverse de manera un poco más elegante abandonando json durante el transporte e ingresando su propio formato. Algo en el espíritu:
union Subject = OrganiationInput | PersonInput input AccountInput { login: String! password: String! subject: Subject! }
Pero esto haría necesario escribir analizadores adicionales para este negocio.
4. Genéricos
¿Qué hay de malo con GraphQL con genéricos? Y todo es simple, no lo son. Tomemos una consulta de índice CRUD típica con una paginación o cursor; no es importante. Daré un ejemplo con paginación.
input Pagination { page: UInt, perPage: UInt, } type Query { users(pagination: Pagination): PageOfUsers! } type PageOfUsers { total: UInt items: [User!]! }
y ahora para organizaciones
type Query { organizations(pagination: Pagination): PageOfOrganizations! } type PageOfOrganizations { total: UInt items: [Organization!]! }
y así sucesivamente ... cómo me gustaría tener genéricos para esto
type PageOf<T> { total: UInt items: [T!]! }
entonces solo escribiría
type Query { users(page: UInt, perPage: UInt): PageOf<User>! }
Sí toneladas de aplicaciones! ¿Debería contarte sobre los genéricos?
5. Espacios de nombres
Ellos tampoco están allí. Cuando el número de tipos en el sistema supera el ciento y medio, la probabilidad de colisiones de nombres tiende al cien por ciento.
Y aparecen todo tipo de Service_GuideNDriving_Standard_Model_Input
. No estoy hablando de espacios de nombres completos en diferentes puntos finales, como en SOAP (sí, sí, es terrible, pero los espacios de nombres están hechos allí perfectamente). Y al menos varios esquemas en un punto final con la capacidad de "buscar" tipos entre esquemas.
Total
GraphQL es una buena herramienta. Encaja perfectamente en una arquitectura de microservicio tolerante, que está orientada, en primer lugar, a la salida de información y a la entrada simple y determinista.
Si tiene entidades polimórficas para ingresar, puede tener problemas.
La separación de los tipos de entrada y salida, así como la falta de genéricos, generan un montón de garabatos desde cero.
Graphql no es realmente (y a veces nada ) sobre CRUD.
Pero esto no significa que no puedas comerlo :)
En el próximo artículo, quiero hablar sobre cómo lucho (y a veces con éxito) con algunos de los problemas descritos anteriormente.