Jahr des Abenteuers mit Graphen-Python

Hallo allerseits, ich bin ein Python-Entwickler. Letztes Jahr habe ich mit Graphen-Python + Django ORM gearbeitet und während dieser Zeit versucht, ein Werkzeug zu entwickeln, um die Arbeit mit Graphen bequemer zu machen. Als Ergebnis habe ich eine kleine graphene-framework
Codebasis und eine Reihe von Regeln erhalten, die ich gerne teilen möchte.

Was ist Graphen-Python?
Laut graphene-python.org dann:
Graphen-Python ist eine Bibliothek zum einfachen Erstellen von GraphQL-APIs mit Python. Seine Hauptaufgabe besteht darin, eine einfache, aber gleichzeitig erweiterbare API bereitzustellen, um das Leben der Programmierer zu erleichtern.
Seine Hauptaufgabe besteht darin, eine einfache, aber gleichzeitig erweiterbare API bereitzustellen, um das Leben der Programmierer zu erleichtern.
Ja, in Wirklichkeit ist Graphen einfach und erweiterbar, aber es scheint mir zu einfach für große und schnell wachsende Anwendungen. Eine kurze Dokumentation (ich habe stattdessen den Quellcode verwendet - er ist viel ausführlicher) sowie das Fehlen von Standards zum Schreiben von Code machen diese Bibliothek nicht zur besten Wahl für Ihre nächste API.
Wie dem auch sei, ich entschied mich, es im Projekt zu verwenden und stieß glücklicherweise auf eine Reihe von Problemen, nachdem ich die meisten gelöst hatte (dank der reichhaltigen undokumentierten Eigenschaften von Graphen). Einige meiner Lösungen sind rein architektonisch und können ohne mein Framework sofort verwendet werden. Der Rest von ihnen benötigt jedoch noch eine Codebasis.
Dieser Artikel ist keine Dokumentation, sondern in gewissem Sinne eine kurze Beschreibung meines Weges und der Probleme, die ich auf die eine oder andere Weise gelöst habe, mit einer kurzen Begründung für meine Wahl. In diesem Teil habe ich mich mit Mutationen und damit verbundenen Dingen befasst.
Der Zweck dieses Artikels ist es, ein aussagekräftiges Feedback zu erhalten , daher werde ich auf Kritik in den Kommentaren warten!
Hinweis: Bevor Sie den Artikel weiterlesen, empfehle ich Ihnen dringend, sich mit GraphQL vertraut zu machen.
Mutationen
Die meisten Diskussionen über GraphQL konzentrieren sich auf das Abrufen von Daten, aber jede Plattform mit Selbstachtung erfordert auch eine Möglichkeit, die auf dem Server gespeicherten Daten zu ändern.
Beginnen wir mit Mutationen.
Betrachten Sie den folgenden Code:
class UpdatePostMutation(graphene.Mutation): class Arguments: post_id = graphene.ID(required=True) title = graphene.String(required=True) content = graphene.String(required=True) image_urls = graphene.List(graphene.String, required=False) allow_comments = graphene.Boolean(required=True) contact_email = graphene.String(required=True) ok = graphene.Boolean(required=True) errors = graphene.List(graphene.String, required=True) def mutate(_, info, post_id, title, content, image_urls, allow_comments, contact_email): errors = [] try: post = get_post_by_id(post_id) except PostNotFound: return UpdatePostMutation(ok=False, errors=['post_not_found']) if not info.context.user.is_authenticated: errors.append('not_authenticated') if len(title) < TITLE_MIN_LENGTH: errors.append('title_too_short') if not is_email(contact_email): errors.append('contact_email_not_valid') if post.owner != info.context.user: errors.append('not_post_owner') if Post.objects.filter(title=title).exists(): errors.append('title_already_taken') if not errors: post = Utils.update_post(post, title, content, image_urls, allow_comments, contact_email) return UpdatePostMutation(ok=bool(errors), errors=errors)
UpdatePostMutation
ändert den Beitrag mit der angegebenen id
unter Verwendung der übertragenen Daten und gibt Fehler zurück, wenn einige Bedingungen nicht erfüllt sind.
Man muss sich nur diesen Code ansehen, da er sichtbar wird, dass er nicht erweiterbar und nicht unterstützbar ist, weil:
- Zu viele Argumente der
mutate
Funktion, deren Anzahl sich erhöhen kann, selbst wenn wir weitere zu bearbeitende Felder hinzufügen möchten. - Damit Mutationen auf der Client-Seite gleich aussehen, müssen sie
errors
und ok
, damit ihr Status und was immer zu verstehen ist. - Suchen und Abrufen eines Objekts in
mutate
Funktion. Die Mutationsfunktion arbeitet mit Fasten, und wenn sie nicht vorhanden ist, sollte die Mutation nicht auftreten. - Überprüfen von Berechtigungen in einer Mutation. Eine Mutation sollte nicht auftreten, wenn der Benutzer nicht das Recht dazu hat (einen Beitrag bearbeiten).
- Ein nutzloses erstes Argument (eine Wurzel , die für Felder der obersten Ebene immer
None
, was unsere Mutation ist). - Eine unvorhersehbare Reihe von Fehlern: Wenn Sie keinen Quellcode oder keine Dokumentation haben, wissen Sie nicht, welche Fehler diese Mutation zurückgeben kann, da sie nicht im Schema berücksichtigt werden.
- Es gibt zu viele Vorlagenfehlerprüfungen, die direkt in der
mutate
werden, bei der die Daten geändert werden, und nicht eine Vielzahl von Prüfungen. Die ideale mutate
sollte aus einer Zeile bestehen - einem Aufruf der Nachbearbeitungsfunktion.
Kurz gesagt, mutate
sollte Daten ändern , anstatt sich um Aufgaben von Drittanbietern wie den Zugriff auf Objekte und die Überprüfung von Eingaben zu kümmern. Unser Ziel ist es, zu etwas zu gelangen wie:
def mutate(post, info, input): post = Utils.update_post(post, **input) return UpdatePostMutation(post=post)
Schauen wir uns nun die obigen Punkte an.
Benutzerdefinierte Typen
Das email
Feld wird als Zeichenfolge übergeben, während es sich um eine Zeichenfolge eines bestimmten Formats handelt . Jedes Mal, wenn die API eine E-Mail empfängt, muss sie ihre Richtigkeit überprüfen. Die beste Lösung wäre also, einen benutzerdefinierten Typ zu erstellen.
class Email(graphene.String):
Dies mag offensichtlich erscheinen, ist aber erwähnenswert.
Eingabetypen
Verwenden Sie Eingabetypen für Ihre Mutationen. Auch wenn sie an anderen Orten nicht wiederverwendet werden können. Dank der Eingabetypen werden Abfragen kleiner, sodass sie leichter zu lesen und schneller zu schreiben sind.
class UpdatePostInput(graphene.InputObjectType): title = graphene.String(required=True) content = graphene.String(required=True) image_urls = graphene.List(graphene.String, required=False) allow_comments = graphene.Boolean(required=True) contact_email = graphene.String(required=True)
An:
mutation( $post_id: ID!, $title: String!, $content: String!, $image_urls: String!, $allow_comments: Boolean!, $contact_email: Email! ) { updatePost( post_id: $post_id, title: $title, content: $content, image_urls: $image_urls, allow_comments: $allow_comments, contact_email: $contact_email, ) { ok } }
Nachher:
mutation($id: ID!, $input: UpdatePostInput!) { updatePost(id: $id, input: $input) { ok } }
Der Mutationscode ändert sich zu:
class UpdatePostMutation(graphene.Mutation): class Arguments: input = UpdatePostInput(required=True) id = graphene.ID(required=True) ok = graphene.Boolean(required=True) errors = graphene.List(graphene.String, required=True) def mutate(_, info, input, id):
Basismutationsklasse
Wie in Absatz 2 erwähnt, müssen Mutationen errors
und ok
damit ihr Status und die Ursachen immer verstanden werden können . Es ist einfach genug, wir erstellen eine Basisklasse:
class MutationPayload(graphene.ObjectType): ok = graphene.Boolean(required=True) errors = graphene.List(graphene.String, required=True) query = graphene.Field('main.schema.Query', required=True) def resolve_ok(self, info): return len(self.errors or []) == 0 def resolve_errors(self, info): return self.errors or [] def resolve_query(self, info): return {}
Ein paar Anmerkungen:
Dies ist sehr praktisch, wenn der Client nach Abschluss der Mutation einige Daten aktualisiert und den Unterstützer nicht auffordern möchte, diesen gesamten Satz zurückzugeben. Je weniger Code Sie schreiben, desto einfacher ist die Wartung. Ich habe diese Idee von hier übernommen .
Mit der Basismutationsklasse wird der Code zu:
class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput(required=True) id = graphene.ID(required=True) def mutate(_, info, input, id):
Wurzelmutationen
Unsere Mutationsanfrage sieht jetzt so aus:
mutation($id: ID!, $input: PostUpdateInput!) { updatePost(id: $id, input: $input) { ok } }
Es ist keine gute Praxis, alle Mutationen in einem globalen Bereich zu enthalten. Hier sind einige Gründe warum:
- Mit der wachsenden Anzahl von Mutationen wird es immer schwieriger, die gewünschte Mutation zu finden.
- Aufgrund eines Namespace ist es erforderlich, "den Namen seines Moduls" in den Namen der Mutation aufzunehmen, z. B. "
Post
update
. - Sie müssen
id
als Argument an die Mutation übergeben.
Ich schlage vor, Wurzelmutationen zu verwenden . Ihr Ziel ist es, diese Probleme zu lösen, indem Mutationen in separate Bereiche unterteilt und Mutationen von der Logik des Zugriffs auf Objekte und der Zugriffsrechte auf diese befreit werden.
Die neue Anfrage sieht folgendermaßen aus:
mutation($id: ID!, $input: PostUpdateInput!) { post(id: $id) { update(input: $input) { ok } } }
Die Anforderungsargumente bleiben gleich. Jetzt wird die Änderungsfunktion innerhalb des post
"aufgerufen", wodurch die folgende Logik implementiert werden kann:
- Wenn
id
nicht an post
, wird {}
. Auf diese Weise können Sie weiterhin Mutationen innerhalb durchführen. Wird für Mutationen verwendet, für die kein Stammelement erforderlich ist (z. B. zum Erstellen von Objekten). - Wenn
id
wird, wird das entsprechende Element abgerufen. - Wenn das Objekt nicht gefunden wird, wird
None
zurückgegeben, und dies schließt die Anforderung ab. Die Mutation wird nicht aufgerufen. - Wenn das Objekt gefunden wird, überprüfen Sie die Rechte des Benutzers, um es zu bearbeiten.
- Wenn der Benutzer keine Rechte hat, wird
None
zurückgegeben und die Anforderung abgeschlossen. Die Mutation wird nicht aufgerufen. - Wenn der Benutzer Rechte hat, wird das gefundene Objekt zurückgegeben und die Mutation erhält es als root - das erste Argument.
Daher ändert sich der Mutationscode zu:
class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput() def mutate(post, info, input): if post is None: return None errors = [] if not info.context.user.is_authenticated: errors.append('not_authenticated') if len(title) < TITLE_MIN_LENGTH: errors.append('title_too_short') if Post.objects.filter(title=title).exists(): errors.append('title_already_taken') if not errors: post = Utils.update_post(post, **input.__dict__) return UpdatePostMutation(errors=errors)
- Die Wurzel der Mutation - das erste Argument - ist jetzt ein Objekt vom Typ
Post
, über das die Mutation durchgeführt wird. - Die Autorisierungsprüfung wurde in den Stammmutationscode verschoben.
Wurzelmutationscode:
class PostMutationRoot(MutationRoot): class Meta: model = Post has_permission = lambda post, user: post.owner == user update = UpdatePostMutation.Field()
Fehlerschnittstelle
Um eine Reihe von Fehlern vorhersehbar zu machen, müssen sie im Design berücksichtigt werden.
- Da Mutationen mehrere Fehler zurückgeben können, sollten Fehler eine Liste sein.
- Da Fehler durch verschiedene Typen dargestellt werden, muss für eine bestimmte Mutation eine bestimmte
Union
Fehlern existieren. - Damit die Fehler einander ähnlich bleiben, müssen sie die Schnittstelle
ErrorInterface
. ErrorInterface
wir sie ErrorInterface
. Lassen Sie es zwei Felder enthalten: ok
und message
.
Daher müssen Fehler vom Typ [SomeMutationErrorsUnion]!
. Alle Untertypen von SomeMutationErrorsUnion
müssen ErrorInterface
implementieren.
Wir bekommen:
class NotAuthenticated(graphene.ObjectType): message = graphene.String(required=True, default_value='not_authenticated') class Meta: interfaces = [ErrorInterface, ] class TitleTooShort(graphene.ObjectType): message = graphene.String(required=True, default_value='title_too_short') class Meta: interfaces = [ErrorInterface, ] class TitleAlreadyTaken(graphene.ObjectType): message = graphene.String(required=True, default_value='title_already_taken') class Meta: interfaces = [ErrorInterface, ] class UpdatePostMutationErrors(graphene.Union): class Meta: types = [NotAuthenticated, TitleIsTooShort, TitleAlreadyTaken, ]
Es sieht gut aus, aber es gibt zu viel Code. Wir verwenden die Metaklasse, um diese Fehler im laufenden Betrieb zu generieren:
class PostErrors(metaclass=ErrorMetaclass): errors = [ 'not_authenticated', 'title_too_short', 'title_already_taken', ] class UpdatePostMutationErrors(graphene.Union): class Meta: types = [PostErrors.not_authenticated, PostErrors.title_too_short, PostErrors.title_already_taken, ]
Fügen Sie der Mutation die Deklaration der zurückgegebenen Fehler hinzu:
class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput() errors = graphene.List(UpdatePostMutationErrors, required=True) def mutate(post, info, input):
Auf Fehler prüfen
Es scheint mir, dass sich die mutate
nur um die Mutation der Daten kümmern sollte. Um dies zu erreichen, müssen Sie den Code für diese Funktion auf Fehler überprüfen.
Ohne die Implementierung ist hier das Ergebnis:
class UpdatePostMutation(DefaultMutation): class Arguments: input = UpdatePostInput() class Meta: root_required = True authentication_required = True
Vor dem Starten der mutate
Funktion wird jeder Checker (das zweite Element der Mitglieder des checks
Arrays) aufgerufen. Wenn True
zurückgegeben wird, wird der entsprechende Fehler gefunden. Wenn keine Fehler gefunden werden, wird die mutate
Funktion aufgerufen.
Ich werde erklären:
- Überprüfungsfunktionen verwenden dieselben Argumente wie
mutate
. - Validierungsfunktionen sollten
True
wenn ein Fehler gefunden wird. - Berechtigungsprüfungen und das Vorhandensein des Stammelements sind recht allgemein gehalten und in den
Meta
Flags aufgeführt. authentication_required
fügt eine Berechtigungsprüfung hinzu, wenn True
.root_required
fügt eine Prüfung " root is not None
" hinzu.UpdatePostMutationErrors
nicht mehr erforderlich. Abhängig von den Fehlerklassen des checks
Arrays wird im checks
eine Vereinigung möglicher Fehler erstellt.
Generika
DefaultMutation
im letzten Abschnitt verwendete pre_mutate
fügt eine pre_mutate
Methode hinzu, mit der Sie die Eingabeargumente ändern können, bevor Sie nach Fehlern pre_mutate
und dementsprechend die Mutation aufrufen.
Es gibt auch ein generisches Starter-Kit, das den Code kürzer macht und das Leben erleichtert.
Hinweis: Derzeit ist der generische Code spezifisch für Django ORM
Kreatemutation
Benötigt einen der model
oder create_function
. Standardmäßig sieht create_function
aus:
model._default_manager.create(**data, owner=user)
Dies mag unsicher aussehen, aber vergessen Sie nicht, dass es in graphql eine integrierte Typprüfung sowie Mutationen gibt.
Es bietet auch eine post_mutate
Methode, die nach create_function
mit Argumenten (instance_created, user)
aufgerufen wird, deren Ergebnis an den Client zurückgegeben wird.
Updatemutation
Ermöglicht das update_function
. Default:
def default_update_function(instance, user=None, **data): instance.__dict__.update(data) instance.save() return instance
root_required
ist standardmäßig True
.
Es bietet auch eine post_mutate
Methode, die nach update_function
mit Argumenten (instance_updated, user)
aufgerufen wird, deren Ergebnis an den Client zurückgegeben wird.
Und das brauchen wir!
Endgültiger Code:
class UpdatePostMutation(UpdateMutation): class Arguments: input = UpdatePostInput() class Meta: checks = [ ( PostErrors.title_too_short, lambda post, input: len(input.title) < TITLE_MIN_LENGTH ), ( PostErrors.title_already_taken, lambda post, input: Post.objects.filter(title=input.title).exists() ), ]
DeleteMutation
Ermöglicht das delete_function
. Default:
def default_delete_function(instance, user=None, **data): instance.delete()
Fazit
Dieser Artikel berücksichtigt nur einen Aspekt, obwohl er meiner Meinung nach der komplexeste ist. Ich habe einige Gedanken zu Resolvern und Typen sowie zu allgemeinen Dingen in Graphen-Python.
Es fällt mir schwer, mich selbst als erfahrenen Entwickler zu bezeichnen, daher freue ich mich über Feedback und Vorschläge.
Den Quellcode finden Sie hier .