Django 2.0+ Pfadkonverter

Hallo!



Das Routing in Django aus der zweiten Version des Frameworks erhielt ein wunderbares Tool - Konverter. Mit diesem Tool wurde es möglich, nicht nur die Parameter in den Routen flexibel zu konfigurieren, sondern auch die Verantwortungsbereiche der Komponenten zu trennen.



Mein Name ist Alexander Ivanov, ich bin Mentor bei Yandex.Practicum an der Backend- Entwicklungsfakultät und leitender Entwickler am Computer Modeling Laboratory. In diesem Artikel werde ich Sie durch die Routenkonverter von Django führen und Ihnen die Vorteile ihrer Verwendung zeigen. Zunächst einmal sind die Grenzen der Anwendbarkeit:











  1. Django Version 2.0+;
  2. Die Registrierung der Routen sollte mit erfolgen django.urls.path



    .


Wenn eine Anfrage beim Django-Server eintrifft, durchläuft sie zuerst die Middleware-Kette und dann wird der URLResolver ( Algorithmus ) aktiviert . Die Aufgabe des letzteren ist es, eine geeignete in der Liste der registrierten Routen zu finden.



Für eine inhaltliche Analyse schlage ich vor, die folgende Situation zu berücksichtigen: Es gibt mehrere Endpunkte, die für ein bestimmtes Datum unterschiedliche Berichte erstellen sollten. Nehmen wir an, die Endpunkte sehen folgendermaßen aus:



users/21/reports/2021-01-31/
teams/4/reports/2021-01-31/
      
      







Was würden die Routen in urls.py



? Zum Beispiel so:



path('users/<id>/reports/<date>/', user_report, name='user_report'),
path('teams/<id>/reports/<date>/', team_report, name='team_report'),
      
      





Jedes Element in < >



ist ein Anforderungsparameter und wird an den Handler übergeben.

Wichtig: Der Name des Parameters bei der Registrierung der Route und der Name des Parameters im Handler müssen übereinstimmen.


Dann hätte jeder Handler so etwas (achten Sie auf Typanmerkungen):



def user_report(request, id: str, date: str):
   try:
       id = int(id)
       date = datetime.strptime(date, '%Y-%m-%d')
   except ValueError:
       raise Http404()
  
   # ...
      
      





Dies ist jedoch keine königliche Angelegenheit - einen solchen Codeblock für jeden Handler zu kopieren und einzufügen. Es ist sinnvoll, diesen Code in eine Hilfsfunktion zu verschieben:



def validate_params(id: str, date: str) -> (int, datetime):
   try:
       id = int(id)
       date = datetime.strptime(date, '%Y-%m-%d')
   except ValueError:
       raise Http404('Not found')
   return id, date
      
      





Und in jedem Handler wird diese Hilfsfunktion einfach aufgerufen:



def user_report(request, id: str, date: str):
   id, date = validate_params(id, date)
  
   # ...
      
      





Im Allgemeinen ist dies bereits verdaulich. Die Hilfsfunktion gibt entweder die korrekten Parameter der erforderlichen Typen zurück oder bricht den Handler ab. Alles scheint in Ordnung zu sein.



Tatsächlich habe ich Folgendes getan: Ich habe einen Teil der Verantwortung für die Entscheidung, ob dieser Handler für diese Route ausgeführt werden soll oder nicht, vom URLResolver auf den Handler selbst verlagert. Es stellt sich heraus, dass URLResolver seine Arbeit schlecht gemacht hat und meine Handler nicht nur nützliche Arbeit leisten müssen, sondern auch entscheiden, ob sie es überhaupt tun sollen. Dies ist ein klarer Verstoß gegen das SOLID- Prinzip der alleinigen Verantwortung . Dies wird nicht funktionieren. Wir müssen uns verbessern.



Standardkonverter



Django bietet Standard- Routenkonverter . Es ist ein Mechanismus zum Bestimmen, ob ein Teil der Route vom URLResolver selbst geeignet ist oder nicht. Ein schöner Bonus: Der Konverter kann den Typ des Parameters ändern, was bedeutet, dass der Typ, den wir benötigen, sofort zum Handler und nicht zum String gelangen kann.



Konverter werden vor dem Parameternamen in der Route angegeben, getrennt durch einen Doppelpunkt. Tatsächlich haben alle Parameter einen Konverter. Wenn dieser nicht explizit angegeben wird, wird der Konverter standardmäßig verwendet str



.



Achtung: Einige Konverter sehen in Python wie Typen aus, daher scheint es, als wären sie normale Casts, aber das sind sie nicht - zum Beispiel gibt es keine Standardkonverter float



oder bool



. Später werde ich Ihnen zeigen, was ein Konverter ist.




Nach dem Betrachten der Standardkonverter wird klar, wofür id



der Konverter verwendet werden soll int



:



path('users/<int:id>/reports/<date>/', user_report, name='user_report'),
path('teams/<int:id>/reports/<date>/', team_report, name='team_report'),
      
      







Aber was ist mit dem Datum? Es gibt keinen Standardkonverter dafür.



Sie können natürlich ausweichen und dies tun:



'users/<int:id>/reports/<int:year>-<int:month>-<int:day>/'

      
      





In der Tat wurden einige der Probleme behoben, da jetzt garantiert ist, dass das Datum in drei durch Bindestriche getrennten Zahlen angezeigt wird. Sie müssen jedoch weiterhin Problemfälle im Handler behandeln, wenn der Client ein falsches Datum sendet, z. B. 2021-02-29 oder 100-100-100 im Allgemeinen. Dies bedeutet, dass diese Option nicht geeignet ist.



Wir erstellen unseren eigenen Konverter



Django bietet zusätzlich zu Standardkonvertern die Möglichkeit , einen eigenen Konverter zu erstellen und die Konvertierungsregeln nach Ihren Wünschen zu beschreiben.



Dazu müssen Sie zwei Schritte ausführen:



  1. Beschreiben Sie die Klasse des Konverters.
  2. Registrieren Sie den Konverter.


Eine Konverterklasse ist eine Klasse mit bestimmten Attributen und Methoden, die in der Dokumentation beschrieben sind (meiner Meinung nach ist es etwas seltsam, dass die Entwickler keine abstrakte Basisklasse erstellt haben). Die Anforderungen selbst:



  1. Es muss ein Attribut vorhanden sein regex



    , das den regulären Ausdruck beschreibt, um die erforderliche Teilsequenz schnell zu finden. Ich werde Ihnen später zeigen, wie es verwendet wird.
  2. Implementieren Sie eine Methode def to_python(self, value: str)



    zum Konvertieren von einer Zeichenfolge (schließlich ist die übertragene Route immer eine Zeichenfolge) in ein Python-Objekt, das schließlich an den Handler übergeben wird.
  3. Implementieren Sie eine Methode def to_url(self, value) -> str



    zum Zurückkonvertieren von einem Python-Objekt in eine Zeichenfolge (wird beim Aufrufen django.urls.reverse



    oder Markieren verwendet url



    ).


Die Klasse zum Konvertieren des Datums sieht folgendermaßen aus:



class DateConverter:
   regex = r'[0-9]{4}-[0-9]{2}-[0-9]{2}'

   def to_python(self, value: str) -> datetime:
       return datetime.strptime(value, '%Y-%m-%d')

   def to_url(self, value: datetime) -> str:
       return value.strftime('%Y-%m-%d')
      
      





Ich bin gegen Duplizierung, daher werde ich das Datumsformat in ein Attribut einfügen. Es ist einfacher, den Konverter zu warten, wenn ich plötzlich das Datumsformat ändern möchte (oder muss):



class DateConverter:
   regex = r'[0-9]{4}-[0-9]{2}-[0-9]{2}'
   format = '%Y-%m-%d'

   def to_python(self, value: str) -> datetime:
       return datetime.strptime(value, self.format)

   def to_url(self, value: datetime) -> str:
       return value.strftime(self.format)
      
      





Die Klasse wird beschrieben, daher ist es Zeit, sie als Konverter zu registrieren. Dies geschieht ganz einfach: In der Funktion müssen register_converter



Sie die beschriebene Klasse und den Namen des Konverters angeben, um ihn in Routen verwenden zu können:



from django.urls import register_converter
register_converter(DateConverter, 'date')
      
      





Jetzt können Sie die Routen in beschreiben urls.py



(ich habe den Namen des Parameters absichtlich geändert dt



, um den Eintrag nicht zu verwirren date:date



):



path('users/<int:id>/reports/<date:dt>/', user_report, name='user_report'),
path('teams/<int:id>/reports/<date:dt>/', team_report, name='team_report'),
      
      





Jetzt ist garantiert, dass die Handler nur aufgerufen werden, wenn der Konverter ordnungsgemäß funktioniert. Dies bedeutet, dass die Parameter des erforderlichen Typs zum Handler gelangen:



def user_report(request, id: int, dt: datetime):
   #     
   #      
      
      





Sieht super aus! Und das ist so, können Sie überprüfen.



Unter der Haube



Wenn Sie genau hinschauen, stellt sich eine interessante Frage: Nirgendwo wird überprüft, ob das Datum korrekt ist. Ja, es gibt eine reguläre Saison, aber ein falsches Datum ist auch dafür geeignet, zum Beispiel 2021-01-77, was bedeutet, to_python



dass ein Fehler darin sein muss. Warum funktioniert es?



Darüber sage ich: "Spielen Sie nach den Regeln des Frameworks, und es wird für Sie spielen." Frameworks übernehmen eine Reihe gemeinsamer Aufgaben. Wenn das Framework etwas nicht kann, bietet ein gutes Framework die Möglichkeit, seine Funktionalität zu erweitern. Daher sollten Sie sich nicht mit dem Fahrradbau beschäftigen. Es ist besser zu sehen, wie sich das Framework bietet, um seine eigenen Fähigkeiten zu verbessern.



Django verfügt über ein Routing-Subsystem mit der Möglichkeit, Konverter hinzuzufügen, die sich um den Methodenaufruf kümmern to_python



und Fehler abfangen ValueError



.



Hier ist der Code aus dem Django-Routing-Subsystem ohne Änderungen (Version 3.1, Datei django/urls/resolvers.py



, Klasse RoutePattern



, Methode match



):



match = self.regex.search(path)
if match:
   # RoutePattern doesn't allow non-named groups so args are ignored.
   kwargs = match.groupdict()
   for key, value in kwargs.items():
       converter = self.converters[key]
       try:
           kwargs[key] = converter.to_python(value)
       except ValueError:
           return None
   return path[match.end():], (), kwargs
return None

      
      





Der erste Schritt besteht darin, mithilfe eines regulären Ausdrucks nach Übereinstimmungen in der vom Client übertragenen Route zu suchen. Diejenige regex



, die in der Konverterklasse definiert ist, nimmt an der Formation teil self.regex



, nämlich sie wird anstelle des Ausdrucks in spitzen Klammern <>



in der Route ersetzt.



Beispielsweise,
users/<int:id>/reports/<date:dt>/
      
      



einbiegen in

^users/(?P<id>[0-9]+)/reports/(?P<dt>[0-9]{4}-[0-9]{2}-[0-9]{2})/$
      
      





Am Ende genauso regelmäßig ab DateConverter



.



Dies ist eine schnelle Suche, oberflächlich. Wenn keine Übereinstimmung gefunden wird, ist die Route definitiv nicht geeignet. Wenn sie gefunden wird, ist sie möglicherweise eine geeignete Route. Dies bedeutet, dass Sie mit der nächsten Überprüfungsstufe beginnen müssen.



Jeder Parameter verfügt über einen eigenen Konverter, mit dem die Methode aufgerufen wird to_python



. Und hier ist das Interessanteste: Der Anruf wird to_python



eingepackt try/except



und Tippfehler werden abgefangen ValueError



. Deshalb funktioniert der Konverter auch bei einem falschen Datum: Ein Fehler fällt ValueError



, und dies wird berücksichtigt, damit die Route nicht passt.



Also im Fall von DateConverter



Zum Glück können wir sagen: Bei einem falschen Datum fällt ein Fehler des gewünschten Typs. Wenn ein Fehler eines anderen Typs auftritt, gibt Django eine Antwort von 500 zurück.



Hör nicht auf



Es scheint, dass alles in Ordnung ist, die Konverter funktionieren, die notwendigen Typen sofort zu den Handlern kommen ... Oder nicht sofort?



path('users/<int:id>/reports/<date:dt>/', user_report, name='user_report'),
      
      





Im Handler zum Generieren eines Berichts benötigen Sie ihn wahrscheinlich User



und nicht id



(obwohl dies der Fall sein kann). In meiner hypothetischen Situation wird nur ein Objekt benötigt, um einen Bericht zu erstellen User



. Was stellt sich dann wieder heraus, fünfundzwanzig?



def user_report(request, id: int, dt: datetime):
   user = get_object_or_404(User, id=id)
  
   # ...
      
      





Die Verantwortung wieder auf den Handler verlagern.



Aber jetzt ist klar, was damit zu tun ist: Schreiben Sie Ihren eigenen Konverter! Es stellt sicher, dass das Objekt vorhanden ist, User



und gibt es an den Handler weiter.



class UserConverter:
   regex = r'[0-9]+'

   def to_python(self, value: str) -> User:
       try:
           return User.objects.get(id=value)
       except User.DoesNotExist:
           raise ValueError('not exists') #  ValueError

   def to_url(self, value: User) -> str:
       return str(value.id)
      
      





Nachdem ich die Klasse beschrieben habe, registriere ich sie:



register_converter(UserConverter, 'user')
      
      





Zum Schluss beschreibe ich die Route:



path('users/<user:u>/reports/<date:dt>/', user_report, name='user_report'),
      
      





Das ist besser:



def user_report(request, u: User, dt: datetime):  
   # ...
      
      





Konverter für Modelle können häufig verwendet werden, daher ist es praktisch, die Basisklasse eines solchen Konverters zu erstellen (gleichzeitig habe ich eine Überprüfung auf das Vorhandensein aller Attribute hinzugefügt):



class ModelConverter:
   regex: str = None
   queryset: QuerySet = None
   model_field: str = None

   def __init__(self):
       if None in (self.regex, self.queryset, self.model_field):
           raise AttributeError('ModelConverter attributes are not set')

   def to_python(self, value: str) -> models.Model:
       try:
           return self.queryset.get(**{self.model_field: value})
       except ObjectDoesNotExist:
           raise ValueError('not exists')

   def to_url(self, value) -> str:
       return str(getattr(value, self.model_field))

      
      





Dann wird die Beschreibung des neuen Konverters zum Modell auf eine deklarative Beschreibung reduziert:



class UserConverter(ModelConverter):
   regex = r'[0-9]+'
   queryset = User.objects.all()
   model_field = 'id'

      
      





Ergebnis



Routenkonverter sind ein leistungsstarker Mechanismus, mit dem Sie Ihren Code sauberer gestalten können. Dieser Mechanismus tauchte jedoch erst in der zweiten Version von Django auf - vorher mussten wir darauf verzichten. Hierher kamen Hilfsfunktionen des Typs get_object_or_404



, ohne diesen Mechanismus entstehen coole Bibliotheken wie DRF.



Dies bedeutet jedoch nicht, dass Konverter überhaupt nicht verwendet werden sollten. Dies bedeutet, dass es (noch) nicht möglich sein wird, sie überall zu verwenden. Aber wenn möglich, fordere ich Sie auf, sie nicht zu vernachlässigen.



Ich werde eine Einschränkung hinterlassen: Hier ist es wichtig, es nicht zu übertreiben und die Decke nicht in die andere Richtung zu ziehen - Sie müssen die Geschäftslogik nicht in den Konverter übernehmen. Die Frage muss beantwortet werden: Wenn eine solche Route grundsätzlich unmöglich ist, liegt dies im Verantwortungsbereich des Konverters. Wenn eine solche Route möglich ist, aber unter bestimmten Umständen nicht verarbeitet wird, liegt dies bereits in der Verantwortung des Handlers, Serializers oder einer anderen Person, aber definitiv nicht des Konverters.



PS In der Praxis habe ich nur einen Konverter für Datumsangaben erstellt und verwendet, nur den im Artikel gezeigten, da ich fast immer DRF oder GraphQL verwende. Bitte teilen Sie uns mit, ob Sie Routenkonverter verwenden und wenn ja, welche?



All Articles