Was ist los mit GraphQL?

In letzter Zeit wird GraphQL immer beliebter. Anmutige Syntax von Anfragen, Typisierung und Abonnements.


Es scheint: "Hier ist es - wir haben die ideale Sprache für den Datenaustausch gefunden!" ...


Ich entwickle diese Sprache seit mehr als einem Jahr und ich sage Ihnen: Alles ist alles andere als reibungslos. GraphQL hat sowohl unangenehme Momente als auch wirklich grundlegende Probleme im Sprachdesign.


Andererseits wurden die meisten dieser "Entwurfsbewegungen" aus einem bestimmten Grund durchgeführt - dies war auf verschiedene Überlegungen zurückzuführen. In der Tat ist GraphQL nicht jedermanns Sache und möglicherweise nicht das Werkzeug, das Sie überhaupt benötigen. Aber das Wichtigste zuerst.


Ich denke, es lohnt sich, eine kleine Bemerkung darüber zu machen, wo ich diese Sprache verwende. Dies ist ein ziemlich kompliziertes SPA-Admin-Panel, bei dem es sich bei den meisten Vorgängen um nicht triviale CRUDs (komplexe Entitäten) handelt. Ein wesentlicher Teil der Argumentation in diesem Material hängt mit der Art der Anwendung und der Art der verarbeiteten Daten zusammen. Bei Anwendungen eines anderen Typs (oder mit einer anderen Art der Daten) können solche Probleme im Prinzip nicht auftreten.

1. NON_NULL


Dies ist kein ernstes Problem. Dies ist vielmehr eine ganze Reihe von Unannehmlichkeiten im Zusammenhang mit der Organisation der Arbeit mit nullable in GraphQL.


Es gibt funktionale (und nicht nur) Programmiersprachen, ein solches Paradigma sind Monaden. Es gibt also so etwas wie die Monade Maybe (Haskel) oder Option (Scala). Unter dem Strich kann der in einer solchen Monade enthaltene Wert existieren oder nicht (dh null sein). Nun, oder es kann durch Aufzählung implementiert werden, wie in Rust.


Auf die eine oder andere Weise und in den meisten Sprachen macht dieser Wert, der das Original "umschließt", null zu einer zusätzlichen Option zum Hauptwert. Und syntaktisch - es ist immer eine Ergänzung zum Haupttyp. Dies ist nicht immer nur eine separate Typklasse - ist es in einigen Sprachen nur eine Ergänzung in Form eines Suffix oder Präfix ? .


In GraqhQL ist das Gegenteil der Fall. Alle Typen sind standardmäßig nullwertfähig - und dies markiert nicht nur den Typ als nullwertfähig, sondern im Gegenteil die Maybe Monade.


Und wenn wir den Abschnitt zur Selbstbeobachtung des Namensfelds für ein solches Schema betrachten:


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

wir finden:


Bild


In NON_NULL


1.1. AUSGABE


Warum so? Kurz gesagt, es ist mit dem standardmäßigen "toleranten" Sprachdesign verbunden (unter anderem ist es für die Microservice-Architektur geeignet).


Um das Wesentliche dieser "Toleranz" zu verstehen, betrachten Sie ein etwas komplexeres Beispiel, in dem alle zurückgegebenen Werte streng in NON_NULL eingeschlossen sind:


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

Angenommen, wir haben einen Dienst, der eine Liste von Benutzern zurückgibt, und eine separate "Freundschaft" für Mikrodienste, die uns eine Übereinstimmung mit den Freunden des Benutzers zurückgibt. Im Falle eines Ausfalls des "Freundschafts" -Dienstes können wir die Benutzer dann überhaupt nicht auflisten. Notwendigkeit, die Situation zu beheben:


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

Dies ist die Toleranz gegenüber internen Fehlern. Ein Beispiel natürlich weit hergeholt. Aber ich hoffe, dass Sie die Essenz verstanden haben.


Darüber hinaus können Sie Ihr Leben in anderen Situationen ein wenig erleichtern. Angenommen, es gibt Remotebenutzer, und die Kennungen von Freunden können in einer externen, nicht verwandten Struktur gespeichert werden. Wir könnten einfach aussortieren und nur das zurückgeben, was wir haben, aber dann werden wir nicht verstehen können, was genau ausgesondert wurde.


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

Alles ok. Und was ist das Problem?
Im Allgemeinen kein sehr großes Problem - also der Geschmack. Wenn Sie jedoch eine monolithische Anwendung mit einer relationalen Datenbank haben, sind Fehler höchstwahrscheinlich Fehler, und die API sollte so streng wie möglich sein. Hallo, Ausrufezeichen! Wo immer du kannst.


Ich möchte in der Lage sein, dieses Verhalten "umzukehren" und Fragezeichen anstelle von Ausrufezeichen zu setzen.) Es wäre irgendwie vertrauter.


1.2. EINGANG


Aber beim Betreten ist nullable eine ganz andere Geschichte. Dies ist ein Pfosten der Checkbox-Ebene in HTML (ich denke, jeder erinnert sich an diese Nicht-Offensichtlichkeit, wenn das Feld eines nicht aktivierten Checkbox einfach nicht nach hinten gesendet wird).


Betrachten Sie ein Beispiel:


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

So weit, so gut. Update hinzufügen:


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

Und jetzt ist die Frage: Was können wir vom Beschreibungsfeld erwarten, wenn wir den Beitrag aktualisieren? Das Feld kann null sein oder ganz fehlen.


Wenn das Feld fehlt, was muss dann getan werden? Nicht aktualisieren? Oder auf null setzen? Die Quintessenz ist, dass das Zulassen von Null und das Zulassen des Fehlens eines Feldes zwei verschiedene Dinge sind. GraphQL ist jedoch dasselbe.


2. Trennung von Input und Output


Das ist nur Schmerz. Im CRUD-Arbeitsmodell erhalten Sie das Objekt von hinten, "drehen" es und senden es zurück. Grob gesagt ist dies ein und dasselbe Objekt. Aber Sie müssen es nur zweimal beschreiben - für die Ein- und Ausgabe. Und damit kann nichts getan werden, außer einen Codegenerator für dieses Geschäft zu schreiben. Ich würde es vorziehen, nicht die Objekte selbst, sondern die Felder des Objekts in die "Eingabe und Ausgabe" zu trennen. Zum Beispiel Modifikatoren:


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

oder mit Anweisungen:


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

3. Polymorphismus


Die Probleme der Trennung von Typen in Eingabe und Ausgabe sind nicht auf eine doppelte Beschreibung beschränkt. Gleichzeitig können Sie für generische Typen verallgemeinerte Schnittstellen definieren:


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

oder Gewerkschaften


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

Sie können dies nicht für Eingabetypen tun. Hierfür gibt es eine Reihe von Voraussetzungen, die jedoch teilweise darauf zurückzuführen sind, dass json als Datenformat für den Transport verwendet wird. In der Ausgabe wird jedoch das Feld __typename verwendet, um den Typ anzugeben. Warum es beim Betreten unmöglich war, dasselbe zu tun - ist nicht ganz klar. Es scheint mir, dass dieses Problem etwas eleganter gelöst werden könnte, indem man json während des Transports aufgibt und sein eigenes Format eingibt. Etwas im Geiste:


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

Dies würde es jedoch notwendig machen, zusätzliche Parser für dieses Geschäft zu schreiben.


4. Generika


Was ist los mit GraphQL mit Generika? Und alles ist einfach - sie sind es nicht. Nehmen wir eine typische CRUD-Indexabfrage mit einer Paginierung oder einem Cursor - das ist nicht wichtig. Ich werde ein Beispiel mit Paginierung geben.


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

und jetzt für Organisationen


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

und so weiter ... wie ich Generika dafür haben möchte


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

dann würde ich einfach schreiben


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

Ja, jede Menge Anwendungen! Soll ich Ihnen von Generika erzählen?


5. Namespaces


Sie sind auch nicht da. Wenn die Anzahl der Typen im System mehr als anderthalbhundert beträgt, liegt die Wahrscheinlichkeit von Namenskollisionen bei hundert Prozent.


Und alle Arten von Service_GuideNDriving_Standard_Model_Input . Ich spreche nicht von vollwertigen Namespaces an verschiedenen Endpunkten, wie in SOAP (ja, ja, es ist schrecklich, aber die Namespaces sind dort perfekt gemacht). Und mindestens mehrere Schemata auf einem Endpunkt mit der Fähigkeit, Typen zwischen Schemata zu "fummeln".


Insgesamt


GraphQL ist ein gutes Werkzeug. Es passt perfekt zu einer toleranten Microservice-Architektur, die sich vor allem an der Informationsausgabe und der einfachen deterministischen Eingabe orientiert.


Wenn Sie polymorphe Entitäten eingeben müssen, können Probleme auftreten.
Die Trennung von Eingabe- und Ausgabetypen sowie das Fehlen von Generika erzeugen eine Reihe von Kritzeleien von Grund auf neu.


In Graphql geht es nicht wirklich (und manchmal überhaupt nicht ) um CRUD.


Das heißt aber nicht, dass du es nicht essen kannst :)


Im nächsten Artikel möchte ich darüber sprechen, wie ich mit einigen der oben beschriebenen Probleme kämpfe (und manchmal erfolgreich).

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


All Articles