Django2.0 +パスコンバーター

こんにちは!



フレームワークの2番目のバージョンからのDjangoでのルーティングは、すばらしいツールであるコンバーターを受け取りました。このツールを追加することで、ルートのパラメータを柔軟に設定できるだけでなく、コンポーネントの責任範囲を分離することも可能になりました。



私の名前はアレクサンドル・イワノフです。Yandexのメンターです。バックエンド開発学部の 実習とコンピューターモデリングラボの主任開発者です。この記事では、Djangoのルートコンバーターについて説明し、それらを使用する利点を示します。 最初に行うことは、適用範囲の限界です。











  1. Djangoバージョン2.0以降;
  2. ルートの登録はで行う必要がありdjango.urls.path



    ます。


そのため、リクエストがDjangoサーバーに到着すると、最初にミドルウェアチェーンを通過し、次にURLResolver(アルゴリズム)がオンになり ます。後者のタスクは、登録されたルートのリストから適切なルートを見つけることです。



実質的な分析のために、次の状況を検討することを提案します。特定の日付に対して異なるレポートを生成する必要があるエンドポイントがいくつかあります。エンドポイントが次のようになっていると仮定します。



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







ルートは urls.py



何ですか?たとえば、次のようになります。



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





の各項目 < >



はリクエストパラメータであり、ハンドラに渡されます。

重要:ルートを登録するときのパラメーターの名前とハンドラーのパラメーターの名前は一致している必要があります。


次に、各ハンドラーは次のようになります(型の注釈に注意してください)。



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





しかし、これは王室のビジネスではありません-ハンドラーごとにそのようなコードのブロックをコピーして貼り付けます。このコードを補助関数に移動することは合理的です。



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
      
      





そして、各ハンドラーで、このヘルパー関数への簡単な呼び出しがあります。



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





一般的に、これはすでに消化可能です。ヘルパー関数は、必要なタイプの正しいパラメーターを返すか、ハンドラーを中止します。すべてが順調のようです。



しかし実際には、これが私がしたことです。このハンドラーをこのルートで実行するかどうかを決定する責任の一部を、URLResolverからハンドラー自体に移しました。URLResolverの機能が不十分であることが判明し、私のハンドラーは有用な作業を行う必要があるだけでなく、それを行うべきかどうかも決定します。これはSOLIDの単独責任の原則に対する明らかな違反 です これは機能しません。改善する必要があります。



標準コンバーター



Djangoは標準のルートコンバーターを提供します 。これは、URLResolver自体によって、ルートの一部が適切かどうかを判断するためのメカニズムです。素晴らしいボーナス:コンバーターはパラメーターのタイプを変更できます。つまり、必要なタイプは文字列ではなくハンドラーにすぐに届きます。



コンバーターは、ルート内のパラメーター名の前にコロンで区切って指定されます。実際、すべてのパラメーターにはコンバーターがあり、明示的に指定されていない場合は、コンバーターがデフォルトで使用されます str







注意:一部のコンバーターはPythonの型のように見えるため、通常のキャストのように見えるかもしれませんが、そうではありませんたとえば、標準のコンバーターfloat



はありません bool



後でコンバーターとは何かをお見せします。




標準のコンバーターを見た後id



、コンバーターを何に使用するかが明らかになり ます int







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







しかし、日付はどうですか?そのための標準的なコンバーターはありません。



もちろん、回避してこれを行うことができます。



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

      
      





実際、日付がハイフンで区切られた3つの数字で表示されることが保証されているため、いくつかの問題は解消されました。ただし、クライアントが誤った日付を送信した場合は、ハンドラーで問題のケースを処理する必要があります。たとえば、一般に2021-02-29または100-100-100です。これは、このオプションが適切でないことを意味します。



独自のコンバーターを作成します



Djangoは、標準のコンバーターに加えて、独自のコンバーターを作成し、必要に応じて変換ルールを記述する機能提供 します。



これを行うには、次の2つの手順を実行する必要があります。



  1. コンバータのクラスを説明してください。
  2. コンバーターを登録します。


コンバータークラスは、ドキュメントに記載されている特定の属性とメソッドのセットを持つクラスです(私の意見では、開発者が基本抽象クラスを作成しなかったのは少し奇妙です)。要件自体:



  1. regex



    必要なサブシーケンスをすばやく見つけるには、正規表現を説明する属性が必要です。後でどのように使用されるかをお見せします。
  2. def to_python(self, value: str)



    文字列(結局のところ、送信されるルートは常に文字列です)からPythonオブジェクトに変換するメソッド実装します。これは、最終的にハンドラーに渡されます。
  3. def to_url(self, value) -> str



    Pythonオブジェクトから文字列に変換するメソッド実装します(呼び出しdjango.urls.reverse



    またはタグ付け時に使用されますurl



    )。


日付を変換するためのクラスは次のようになります。



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')
      
      





私は重複に反対しているので、日付形式を属性に入れます-突然日付形式を変更したい(または必要な)場合は、コンバーターを保守する方が簡単です。



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)
      
      





クラスが記述されているので、コンバーターとして登録する時が来ました。これは非常に簡単に実行register_converter



できます。関数 で、ルートで使用するには、記述されたクラスとコンバーターの名前を指定する必要があります。



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





これで、次のルートを記述できます urls.py



dt



エントリを混乱させないよう に、パラメータの名前を意図的にに変更しました 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'),
      
      





これで、コンバーターが正しく機能する場合にのみハンドラーが呼び出されることが保証されます。つまり、必要なタイプのパラメーターがハンドラーに送られます。



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





すごいですね!そして、これはそうです、あなたはチェックすることができます。



フードの下



よく見ると、興味深い質問が発生します。日付が正しいかどうかのチェックはどこにもありません。はい、定期的なシーズンがto_python



ありますが、2021-01-77のように、間違った日付も適しています。これは、エラーが発生している必要があることを意味 します。なぜそれが機能するのですか?



これについて私は言います:「フレームワークのルールに従ってください、そしてそれはあなたのためにプレーします。」フレームワークは多くの一般的なタスクを引き受けます。フレームワークが何かを実行できない場合、優れたフレームワークはその機能を拡張する機会を提供します。したがって、自転車の製造に従事するべきではありません。フレームワークが独自の機能を向上させるためにどのように提供するかを確認することをお勧めします。



Djangoには、メソッド呼び出しを処理するコンバーターを追加する機能を備えたルーティングサブシステムがあります to_python



とエラーをキャッチし ValueError



ます。



変更を加えていないDjangoルーティングサブシステムのコードは次のとおりです(バージョン3.1、ファイル django/urls/resolvers.py



、クラス RoutePattern



、メソッド 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

      
      





最初のステップは、正規表現を使用してクライアントから送信されたルートで一致するものを検索することです。1 regex



形成におけるコンバータクラス参加に定義され self.regex



、すなわち、それが代わりに角括弧内の式の置換されている <>



経路です。



例えば、
users/<int:id>/reports/<date:dt>/
      
      



に変わる

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





結局、からの同じレギュラー DateConverter







これは、表面的なクイック検索です。一致するものが見つからない場合、そのルートは間違いなく適切ではありませんが、見つかった場合、それは潜在的に適切なルートです。これは、検証の次の段階を開始する必要があることを意味します。



各パラメーターには、メソッドを呼び出すために使用される独自のコンバーターがあり to_python



ます。そして、これが最も興味深いことです。呼び出しはに to_python



ラップされ try/except



、タイプエラーがキャッチされ ValueError



ます。そのため、日付が間違っていてもコンバーターは動作します。エラーが発生 ValueError



し、ルートが適合しないと見なされます。



だからの場合 DateConverter



、幸運なことに言えます。日付が正しくない場合、必要なタイプのエラーが発生します。別のタイプのエラーがある場合、Djangoは500応答を返します。



やめないで



すべてが正常で、コンバーターが機能していて、必要なタイプがすぐにハンドラーに届くようです...またはすぐにではありませんか?



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





レポートを生成するためのハンドラーでは、おそらく必要でありUser



必要で はありません id



(ただし、そうなる場合もあります)。私の仮定の状況では、レポートを作成するために必要なのはオブジェクトだけ User



です。それでは、再び25になりますか?



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





責任を再びハンドラーに移します。



しかし今では、それをどうするかが明確になっています。独自のコンバーターを作成してください。オブジェクトが存在することを確認し、 User



それをハンドラーに渡します。



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)
      
      





クラスについて説明した後、登録します。



register_converter(UserConverter, 'user')
      
      





最後に、ルートについて説明します。



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





それは良いです:



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





モデルのコンバーターは頻繁に使用できるため、そのようなコンバーターの基本クラスを作成すると便利です(同時に、すべての属性の存在のチェックを追加しました)。



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))

      
      





次に、モデルへの新しいコンバーターの説明は、宣言的な説明になります。



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

      
      





結果



ルートコンバーターは、コードをよりクリーンにするのに役立つ強力なメカニズムです。しかし、このメカニズムはDjangoの2番目のバージョンでのみ登場しました-それ以前はそれなしでやらなければなりませんでした。これがこのタイプの補助関数の get_object_or_404



由来です。このメカニズムがなければ、DRFのようなクールなライブラリが作成されます。



しかし、これはコンバーターをまったく使用すべきではないという意味ではありません。これは、(まだ)どこでもそれらを使用することが不可能であることを意味します。しかし、可能であれば、それらを無視しないことをお勧めします。



注意点を1つ残しておきます。ここでは、やりすぎたり、ブランケットを反対方向にドラッグしたりしないことが重要です。ビジネスロジックをコンバータに取り込む必要はありません。質問に答える必要があります:そのようなルートが原則的に不可能である場合、これはコンバーターの責任範囲です; そのようなルートが可能であるが、特定の状況下でそれが処理されない場合、これはすでにハンドラー、シリアライザー、または他の誰かの責任ですが、コンバーターではありません。



PS実際には、ほとんどの場合DRFまたはGraphQLを使用するため、日付用のコンバーターのみを作成して使用しました。これは、記事に示されているものだけです。ルートコンバーターを使用しているかどうか、使用している場合はどれを使用しているかを教えてください。



All Articles