クラスビューでPUTを介してデータをパースする

クラスビューでPUTを介してデータをパースする

2022年4月12日

リクエストに’データ’属性がない

会社で今回、関数型で設計されていたビューをクラスビューに再設計する際に発生した問題について見ていこうと思います。

Djangoを使っていると少し疑問に思う部分があります。特にREST Frameworkを通じてデータを受け取るときですが

私の場合はAttributeError: request has no attribute 'data'というエラーが発生しました。

まず、私が実装したクラスを見てみましょう。

# urls.py
urlpatterns = [
  path('my_url/', views.MyView.as_view())
]

# views.py 
class MyView(View)
  def put(self, request):
    my_data = request.data
    # my business logic
    

もちろんJavaScriptでは myapp/my_url/に正しくデータを送っていましたが、request.dataという変数は生成されませんでした。

Django Rest Frameworkを学んだことがある方なら、このコードが変だとわかるはずです。

私もわかっています。通常はSerializerを実装して、そのクラスを通じてJSONデータをパースするのが一般的ですよね?

しかし、残念ながら私の状況ではそれができませんでした。

参照できるモデルもなく、ほとんどのモデルと関連のある関数がRaw Queryで直接データベースに接続して実行するクエリだったので、Serializerを独立して構成するのは難しかったのです。

モデルよりSerializerがフロントエンドで受け取っているということになるので、結局それをまた解いてデータを取り出すのであれば、後でモデルを新たに設計する際に新たなリスクが生じるかもしれないと考えました。

ともかく、コードに戻りましょう。結局、上記のコードは正しくデータを受け取ることができず、私は問題があると感じてPyCharm IDEを通じてデバッグしてみました。

結論として、requestにはdataというメンバ変数は全く存在しませんでした。

おかしいですよね?Class ViewではなくFunction Viewを使用して下記のコードのように設計したときは、request.dataはうまく入ってきました。

もちろんGETPOSTで入ってきたデータに関しては、request.POSTrequest.GETのような形式で、別途データをパースして保存する内部メンバ変数が存在しますが、PUTDELETEにはそれが無いというのが本当に不思議でした。

# urls.py
urlpatterns = [
  path('my_url/', views.my_view)
]

# views.py 
  def my_view(self, request):
    my_data = request.data
    # my business logic
    

私はGoogleでclass view django putというキーワードで検索し、次のようなページを見つけることに成功しました。

ブログで答えを探してみましょう。

この後の内容は、このブログを参考にしました。

request.POSTはRESTのPOSTとは違う

このブログに入ってみると、Googleグループで開発者たちが行ったこのような会話を垣間見ることができます。

会話の内容からDjangorequest.POSTRESTを通じて設計されたものではなく、HTMLformmethod="post"として設定して送信したときに受け取るデータを前提としていることがわかります。

ほとんどのformから送信されるPOST形式のケースでは、Content Typemultipart/form-dataに設定されており、エンコーディングがJSONとは異なることを教えています。

Djangorequest.POSTがこの形式のデータをパースすることを前提としているため、多くの場合、予期しないエラーが発生する可能性があることに注意を促しています。

特にREST frameworkを使用してJSON形式でデータを送信しても、request.POSTには正しくデータが入ってきますが、この場合も常にformを通じてエンコードされたデータであるため、無視するようにと言っています。

難しい話ですが、結局requestにはrequest.PUTrequest.DELETEのようなものが無かったのです。

それではどうするか?

実際に私たちが使用しているウェブでは、まず適切にモデルを通じて実装されたSerializerがなく、クラスビューで作成することが目的であるため、request.POSTrequest.GETのような形式でrequest.PUTのようにデータを受け取ると非常に便利だと思われます。

これを実現するために、requestが関数に渡される途中でミドルウェア側でrequestを先行して受け取り、requestの下位プロパティにPUTという変数を追加する形式で進めてみようと思います。

後でPOSTやPUTにcontent-typeapplication/jsonで渡された場合に備えて、JSONというプロパティも作成することにしましょう!

ソリューション

まず皆さんが作成した共通モジュールフォルダがある場合はそこに、ない場合は新たにフォルダを作成し、parsing_middleware.pyと作成しましょう(名前は関係ないので、好きなものにしても構いません)。 次のようなコードを入力してください

私はCOMMONという共通モジュールフォルダを作成し、モジュール間の区別のためにmiddlewareというフォルダを中に入れ、parsing_middleware.pyファイルを作成しました。

# COMMON/middleware/parsing_middleware.py

import json

from django.http import HttpResponseBadRequest
from django.utils.deprecation import MiddlewareMixin


class PutParsingMiddleware(MiddlewareMixin):
    def process_request(self, request):
        if request.method == "PUT" and request.content_type != "application/json":
            if hasattr(request, '_post'):
                del request._post
                del request._files
            try:
                request.method = "POST"
                request._load_post_and_files()
                request.method = "PUT"
            except AttributeError as e:
                request.META['REQUEST_METHOD'] = 'POST'
                request._load_post_and_files()
                request.META['REQUEST_METHOD'] = 'PUT'

            request.PUT = request.POST


class JSONParsingMiddleware(MiddlewareMixin):
    def process_request(self, request):
        if (request.method == "PUT" or request.method == "POST") and request.content_type == "application/json":
            try:
                request.JSON = json.loads(request.body)
            except ValueError as ve:
                return HttpResponseBadRequest("unable to parse JSON data. Error : {0}".format(ve))

そしてsettings.pyに該当ミドルウェアを使用するように設定すれば終わりです。

# my_project_name/settings.py
MIDDLEWARE = [
...
  # PUT, JSONパースのためのミドルウェア追加
  'COMMON.middleware.parsing_middleware.PutParsingMiddleware',
  'COMMON.middleware.parsing_middleware.JSONParsingMiddleware',
...
]

後の悩み

Raw QueryをDjango Model基盤に改善する際に発生する問題についても後で投稿する予定ですが、問題はDjangoのプロジェクト構造のようですね。

さまざまなリファレンスを探しましたが、Djangoのドキュメントでもviews.pymodels.pyが大きくならざるを得ない構造を推奨していますね。

▼ Djangoで提案されるベストプラクティス例

image

pythonでこれが最善か、理想的な構造かはわかりませんが、当面考えつく問題が多いです。

Djangoは独立したアプリにviews.pyにはビジネスロジックおよびコントローラーを、modelsにはDBに関連するモデルを、templateにはHTMLページとCSSJSを入れますが、アプリに機能が追加されればされるほど、一つのファイルが持つ依存性がますます大きくなるでしょう。

しかし、むやみにviews.pyを分離すると、循環参照、依存性がますます大きくなるなど様々な部分で問題が生じ、連鎖的に問題が発生するでしょう。

モジュールはオブジェクト指向モデルに合わせて独立して設計するべきですが、Djangoの場合はSpringのように依存性を管理してくれるIOCコンテナのような良い機能がないので。

独立してモジュールをどのように設計し、良いサービスをどのように作り上げていくかを考えるのが今後の課題です。

引き継ぎのためのドキュメント自動化も重要な課題ですね。

私の悩みとこれらの問題をどのように解決したかを後に投稿しようと思います!

結びに

  • 今日は私が関数型ビューをクラス型ビューに変えた際に発生したエラーの解決プロセスについて学びました。もちろん私と同じ状況にある方は少ないとは思いますが、皆さんもこの投稿を参考にして問題を解決するのに役立てていただければ幸いです。
  • いつでも良い指摘、良い言葉はありがたく受け取ります。
  • ありがとう!