📜 ⬆️ ⬇️

What is wrong with GraphQL

Recently GraphQL is gaining increasing popularity. Elegant query syntax, typing and subscriptions.


It seems: "here it is - we have found the perfect language of data exchange!" ...


I have been developing with the use of this language for more than a year now, and I will tell you: everything is far from being so smooth. In GraphQL there are both uncomfortable moments and truly fundamental problems in the design of the language itself.


On the other hand, most of these “design moves” were made for a reason - this was due to one or other considerations. In fact, GraphQL is not suitable for everyone, and may not be the tool you need. But first things first.


I think it is worth making a small remark about where I use this language. This is a rather complicated SPA-admin, most of the operations in which are fairly nontrivial CRUD (complex entities). A significant part of the argument in this material is connected precisely with the nature of the application and the nature of the data being processed. In applications of another type (or with a different nature of data), such problems may not arise in principle.

1. NON_NULL


This is not a serious problem. Rather, it is a whole series of inconveniences related to how work with nullable is organized in GraphQL.


There are functional (and not only) programming languages, such a paradigm - monads. So, there is such a thing as the Maybe (Haskel) or Option (Scala) monad. The bottom line is that the value contained within such a monad may or may not exist (that is, be null). Well, or it can be implemented through enum, as in Rust.


One way or another, in most languages ​​this value, which "wraps" the original one, makes null an additional option to the main one. And syntactically - it is always an addition to the main type. It is not always just a separate type class - in some languages, is it just a supplement in the form of a suffix or prefix ? .


In GraqhQL, the opposite is true. All types are nullable by default - and this is not just a mark of the type as nullable, this is the Maybe monad in reverse.


And if we consider the introspection section of the name field for such a scheme:


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

then we will find:


image


String type wrapped in NON_NULL


1.1. OUTPUT


Why so? In short, it is connected with the “tolerant” default design of the language (among other things, microservice-friendly architecture).


To understand the essence of this "tolerance", consider a slightly more complicated example in which all returned values ​​are strictly wrapped in NON_NULL:


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

Suppose that we have a service that returns a list of users, and a separate micro-service "friendship", which returns us the mapping for the friends of the user. Then, in case of failure of the service "friendship", we generally can not display a list of users. Need to fix the situation:


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

This is the tolerance for internal errors. An example, of course, contrived. But I hope that you have grasped the essence.


In addition, you can make your life a little easier in other situations. Suppose that there are remote users, and aydishniki friends can be stored in some external unrelated structure. We could just weed out and return only what we have, but then we cannot understand what exactly was screened out.


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

All OK. And what's the problem?
In general, not a very big problem - so tastes. But if you have a monolithic application with a relational database, then most likely errors are really errors, and api should be as strict as possible. Hello exclamation marks! Wherever possible.


I would like to be able to "invert" this behavior, and arrange the question marks, instead of exclamation marks) It would be more habitual.


1.2. INPUT


But when you type, nullable is a different story altogether. This is a jamb at the checkbox level in HTML (I think that everyone remembers this non-obviousness when the unchecked checkbox field is simply not sent to the backend).


Consider an example:


 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! } 

It's okay for now. Add update:


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

And now the question: what can we expect from the description field when updating a post? The field may be null, or it may be absent altogether.


If there is no field, then what should be done? Do not update it? Or set it to null? The bottom line is that resolving the null value and allowing the absence of a field are two different things. However, in GraphQL it’s the same thing.


2. Separation of input and output


This is just a pain. In the CRUD operation model, you receive an object from the back-up "twisting" it, and send it back. Roughly speaking, it is the same object. But you just have to describe it twice - for input and output. And with this nothing can be done, except how to write a code generator for this business. I would prefer to divide into the "input and output" not the objects themselves, but the fields of the object. For example, modifiers:


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

or using directives:


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

3. Polymorphism


The problems of dividing types into inputs and outputs are not limited to a double description. While generic interfaces can be defined for inferred types:


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

or unions


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

You cannot do the same for input types. There are a number of prerequisites for this, but this is partly due to the fact that json is used as the data format for transport. However, in the output, to specify the type, the __typename field is __typename . Why it was impossible to do the same when entering - not very clear. It seems to me that this problem could be solved a little more elegantly, by abandoning json during transport and entering my own format. Something in the spirit of:


 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", } } } 

But this would create the need to write additional parsers for this case.


4. Generics


And what's wrong with GraphQL with generics? And everything is simple - they are not. Let's take a banal index query common to CRUD with a pagination or a cursor, it doesn't matter. I will give an example with pagination.


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

and now for organizations


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

and so on ... how I would like to have generics for this business


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

then I would just write


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

Yes, tons of uses! Should I tell you about generics?


5. Namespaces


They are not there either. When the number of types in the system prevails over a hundred and fifty, the probability of name collisions tends to be 100 percent.


And there are all sorts of Service_GuideNDriving_Standard_Model_Input . I’m not talking about full namespaces on different endpoints, as in SOAP (yes, it’s awful, but the namespaces are fine there). And at least several circuits on one endpoint with the ability to "fumble" types between circuits.


Total


GraphQL is a good tool. It fits perfectly on the tolerant, microservice architecture, which is focused, first of all, on the output of information, and simple, deterministic input.


If you have polymorphic entities on input, you may have problems.
Separation of types of input and output, as well as the absence of generics - generate a bunch of writings from scratch.


Graphql is not quite (and sometimes not ) about CRUD.


But this does not mean that you can not eat it :)


In the following article, I want to talk about how I fight (and sometimes successfully) with some of the problems described above.


')

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


All Articles