7.3 こんな時どうする?

 7.2では、非常に単純なモデルや処理の例を用いて、 Django+DRFの基本的な動作を説明してきましたが、実用的なプログラムはそこまで単純ではありません。
実用プログラムを書く時に、よくありそうなシチュエーション別に、Tipsを書いてみます。

7.3.1 複数のデータを一括で登録したい

 7.2.で説明したgeneric API ViewCreateAPIVIewView SetではPOSTのよる単一データの登録はできますが、そのままでは、複数データをまとめて登録する事ができません。複数のデータを一括で登録したい場合にどうするかを説明して行きます。

例として、作業記録用のアプリケーションを作ることを想定して、以下のようなモデルクラスを定義してデータベースにマイグレートしたとします。

start_timeにはその作業を開始した時刻、durationには作業時間の秒数、titleにはその作業の名前を格納します。

まずSerializerとviewsetを定義します。7.2で説明した通りにやると以下のようになります。

しかしこのままでは、

のような単一のデータをPOSTして登録することはできますが、次のような複数のデータをまとめたリストは取り扱うことはできません。

複数データを登録するには、主に2つのやり方があります。

(1)create() 関数をオーバーライドする

一つ目は、ModelViewSetのcreate() 関数をオーバーライドする方法です。(CreateAPIVIewの場合も同じやり方が使えます)

実は、この関数はDRFがデフォルトで提供しているcreate関数とほとんど同じです。唯一の違いは、元のcreate関数の最初の行が、

となっているのに対し、オーバーライドした関数ではmany=isinstance(request.data,list)という引数を渡していることです。isinstance関数は、第一引数で渡されたオブジェクトの型が第二引数で指定したものであればTrueを返します。つまり、渡されたrequest.dataが複数のデータをまとめたリストであればTrueを、そうでなければFalseを返すことになります。

  • get_serializer関数は、many属性が省略されていたりFalseだったした場合serializer_classで指定されたActivitySerializerのインスタンスを返しますが、many=Trueの場合には、DRFが提供するListSerializerクラスのインスタンスを返します。
  • ListSerializerは、リスト形式のデータを扱うための汎用クラスです。ListSerializerが持つ変数「child」にはActivitySerializerクラス、「validated_data」にはrequest.dataからバリデートされたリストデータが格納されており、self.perform_create(serialize)の中でListSerializercreate()関数が呼ばれるとvalidated_dataに入っているリストの各要素ごとにActivitySerializer.createが実行されデータが格納されます。

 このやり方の問題点は、渡されたリストの要素の数だけActivitySerializer.createが呼ばれ、その数だけデータベースアクセスが発生することです。データアクセスの処理には時間がかかります。要素数が多くなると登録に長時間かかってしまい、性能が問題になってきます。

(2)複数データ登録用のシリアライザーを作る 

 Djangoが提供するモデルには、複数データをデータベースに一括で登録するための仕組みbulk_createという機能があり、これを使えばデータベースアクセス時間の問題を回避する事ができます。

まず、ListSerializer クラスを継承したCreateActivityListSerializerを定義し、ListSerializerクラスのcreate() 関数をオーバーライドします。create 関数の中の2行目でbulk_create を読んでresult に入っている複数のデータを一括で登録しています。

次にActivitySerializer 内のMetaクラスのlist_serializer_classCreateActivityListSerializer をセットしています。こうしておくと、get_serializer 関数でmany=Trueが渡された時に、デフォルトのListSerializerではなくCreateActivityListSerializerを返してくれます。

ListSerializer

View側は、一つ目のやり方のActivityViewSetで使ったcreateのオーバーライドをそのまま使ってもいいのですが、createはオーバーライドせずその中で呼ばれる、get_serializer関数をオーバーライドし、この中で送られてきたデータが複数かを判断する方法でも動作します。(こちらの方がコードが短くなります)

get_serializerに渡される辞書型のkwargsの中のdataにリクエストとして送られてきたデータが格納されますので、isinstanceでこれがリスト型かどうかを調べ、リスト型であればkwargmanyの値にTrueをセットし、get_serializerを呼び直します。

ActivityViewSetの親クラスのget_serializer(つまりオーバーライドされていない元のget_serializer)を呼び出し、kwargを渡します。manyの値がTrueになっていれば、CreateActivityListSerializerが返ってきます。

上の例ではViewSetの場合で説明しましたが、generic view を使う場合でもやり方は同じです。

7.3.2 GETメソッドで条件検索した結果を返したい

 7.3.1で定義したActivityViewにGETメソッドを送ると複数のデータを返すことができますが、そのままだとデータベースに入っているデータを全て返してしまいます。このままでは、実用的なAPIとしてはあまり使えそうにありません。何らかの検索条件に合ったものだけを返すようにはどうすれば良いでしょう?

例として、「start_time」がある期間に含まれているActivityだけを返すように改造してみます。前提として、期間の開始時期(start)と終了時期(end)はフロントエンドからHTTPリクエストの中に入れられて送付され、期間の開始時期と終了時期は以下のようなフォーマットの文字列で送付されてくるものとします。

ちょっと余談になりますが、URLパラメータに使う文字列は、空白は「+」に置き換え、英数字と「 - . _ ~ 」以外は「パーセント記号と記号のASCIIコードを16進数で表わしたもの」に置き換えて(エンコードして)送信する規則になっています。URLパラメータを含む実際のURLの文字列は以下のようになります。

このようにして送られてきたURLをバックエンド側のviewで処理を行います。元々のclass ActivityViewSetでは、データベースの呼び出しに使うquery

となっていて、これがGeneric Viewで定義されているget_querysetで呼び出されるようになっていました。元々のget_querysetは単純にqueryset に入っている値を返しますが、これを以下のようにオーバーライドしてリクエストで送られて来たパラメータの条件で検索を行うquerysetを生成して返すようにします。また、元々の queryset = Activity.objects.all()の行は必要なくなりますので、削除しても大丈夫です。

Viewクラスのオブジェクトに渡されたURLパラメーターの情報は、デコードされた状態で変数requestに格納されるので、1行目のself.request.query_params.get("start”) や2行目のend_str = self.request.query_params.get("end”) でその値を取り出します。3行目はfilterを使って検索条件を指定しています。start_timeがリクエストされた開始時間より後で、リクエストされた終了時間より前のものを検索するquerysetを作って、これを返しています。

あとは改造前と同じ処理が行われ、この検索条件に合ったものだけがシリアライズされてフロントエンドに返されます。

7.3.3 データベースから取得した情報を加工したり、その他の情報を付加したい

データベースから取得した情報をそのまま返すのではなく、加工したり情報を追加したりして返したい時には、Serializerで情報を加工します。(1)と同じActivityモデルを使った例で説明します。

ActivityViewSetで使ったSerializerを以下に再掲します。

このSerializerではActivityモデルで定義した全てのフィールドを返します。

ここで、Activityモデルが持つフィールドに一つフィールドを追加しようと思います。作業が終了した時刻を’end_time’という名前で付け加えます。end_timeは、start_timedurationを使って計算します。これを _ActivitySerializerという名前のSerializerとして定義したものが以下です。

まず、新たなフィールドend_timeのフィールドを_ActivitySerializerのクラス変数として定義します。フィールドの型はserializers.SerializerMethodFieldで、これを使うとクラス内で定義されている 'get_<フィールド名>'' 関数の戻り値を、変数の値として設定してくれます。Metaクラスのfieldには、’end_time’を含むフィールドのリストを記載します。

get_end_time関数はend_timeフィールドに入る値を返す関数です。第二引数にActivityオブジェクトが渡されますので、このオブジェクトのstart_timedurationを使ってend_timeの入る時刻の文字列を作って返します。

このSerializerを使ったviewの例は以下です。

これにより得られる結果は、以下のようになります。

Activityモデルが持つフィールドの値を加工したい場合にもこの方法が使えます。例として、start_timeはデフォルトではISO8601フォーマットで表示されますが、これを簡略化して’2024/05/04 10:30:20’のようなフォーマットにしてみます。ついでに、end_timeも同じフォーマットにします。

start_timeフィールドの定義では、read_only=Trueとしています。これは、GETメソッドで値を読み出す時にのみ有効にするためです。こうしておくと、POSTによるcreateやPUTによるupdate時には、この定義は使われず、今まで通りの方法でデータを追加したり変更したりできます。

7.3.4 情報を加工してモデルとは全く違う型のデータを返したい

7.3.3 では、Serializerの中で情報を一部加工するやり方を説明しましたが、データベースに入っているデータを処理して、全く違う型のデータに加工して返すような場合は、もっと直接的なやり方をします。

作業記録を表すActivityのデータを基に、作業タイトルごとに合計位時間を求めるAPIを作ってみます。APIが返すデータは下記のようになります。

これを実現するViewのコードは以下の通りで、ListAPIViewlistメソッドをオーバーライドします。

見ての通り、Serializerは使いません。(使う必要はありません。)処理の中で、辞書形式アイテムをリスト化したデータを作って、それをそのままResponse()の引数にして返してしまえばうまくいきます。

 上記の例ではlist()の中で新たなデータ型を生成する処理を行いましたが、実はデータベースへのqueryの中で同等の処理を行うこともできます。

  • values()は指定したフィールドだけを取り出す処理を行います。
  • annotate()は、オブジェクトに新たなフィールドを追加する処理を行います。
  • Sum()は指定したフィールドの値を合計する処理を行います。
  • values(‘title’)とすることで、”title”フィールドだけが取り出され、これにtotalという名前で、titleごとにdurationを合計した値がannotation()で追加されます。

このqueryの結果得られるのは、queryset型のオブジェクトのリストですので、Responseに渡す辞書型のリストに変換するためのSerializerが必要になります。モデルで定義したものとは全く異なるデータ型を返す場合は、serializers.Serializerを継承してクラスを定義します。

結果として、TotalActivityViewは以下のように非常に簡単に書くことができます。

7.3.5 関連した情報を入れ子にして一緒に返したい

 7.2.1で使ったStudentモデルとDepartモデルを再度例として使ってみます。

7.2.3のGeneric API Viewの説明の時に出てきた生徒の一覧データを取得するStudentListViewを再掲します。

これによって得られる結果は以下です。

単純にStudentモデルを返すserializerを使うと、departmentフィールドにはid値が入っているのがわかります。しかし、DepartmentフィールドにDepartmentモデルの情報を入れて返したいこともよくあります。例えばこのような情報です。

このようにするためにserializerを改造します。まず、Departmentモデルに対するSerializerを定義します。

さらに、StudentSerializerdepartmentのフィールドにこのSerializerを定義します。

これで、上記のような入れ子になった情報が一応得られます。

Studentクラスのオブジェクトの’department’フィールドの値には、DepartmentSerializerによって出力されるJSON形式のデータが入っているのがわかります。

 しかし、このやり方ではデータベースへのアクセス回数が大幅に増えてしまうという問題があります。Djangono動作は以下のようになります。

  • Djangoはまず、StudentListViewで定義されたqueryset(Student.objects.all())に従って、Studentクラスのオブジェクト全てをデータベースから取ってきます。
  • その後、Studentクラスのオブジェクトの’department’フィールドに入るデータを作るために、毎回データベースにquery(問い合わせ)を行なってしまいます。つまり、Studentクラスのオブジェクトの数だけqueryを行うことになります。

上記の例では、高々3つのデータしか取ってきませんのでそれほど問題にはなりませんが、データの数が増えるとコストの高いデータベースへのアクセス回数がそれだけ増え、どんどん処理が遅くなっていきます。これを避けるために、StudentListViewで定義したquerysetを修正します。

prefetch_relatedを使うと、関連するオブジェクトの情報が必要になるたびにqueryを行うのではなく、あらかじめ必要な関連オブジェクトの情報を一括してqueryしてくれます。データベースのqueryの回数は、Studentオブジェクトの取得と合わせ計2回のみになります。

7.3.6 Serializerにパラメータを渡したい

 シリアライザーで情報を加工する際に、viewからシリアライザーにパラメータを渡す必要が出てくることがあります。元のGenericAPIViewクラスでは、シリアライザーを呼び出すget_serializer()関数の中で、「context」という情報を作り、これをシリアライザーに渡しています。シリアライザーの中ではこのcontextを介してviewからの情報を得ることができるようになっています。get_serializer()関数の中で作られるcontextは辞書形式のデータで、この中にはキー’view’の値にシリアライザーを呼び出したviewのインスタンス、キー’request’の値にviewが受け取ったrequestオブジェクトが入ります。これにより、viewのインスタンス変数や、requestの中に入っている変数をシリアライザーから参照することができます。

(3)で定義したシリアライザーは、タイムゾーンが"Asia/Tokyo”に固定されていますが、この情報を変数にしてviewからserializerに渡すように改造してみます。

シリアライザーの中のget_start_time()get_end_time()の中でcontextからviewを取り出し、viewの持っているtzone変数の値を参照しています。

この例では、タイムゾーンの情報がviewの中で定義されていますが、フロントエンドからrequestのパラメータとして送られてくる場合も、contextからrequestを取り出して、同様のやり方で値を参照することができます。

7.3.7 ページネーション

ページネーションとは

 検索した結果を表示する際に、項目が多く1ページに収まらないような場合、「ページネーション」と呼ばれる、複数ページに分けて表示を行う方法を使います。

ページネーションの例

このようなGUIを表示するためには、検索した結果データの他に、現在表示されているページ番号と全ページ数、「前」「後」のボタンに紐づけられるリンクなどの情報が必要になりますが、DRFでは、これらの情報を一度に返す実装が非常に簡単にできます。

ページネーションの実装

 まず、ページネーションのために必要な情報を実装します。DRFは元々PageNumeberPaginationというクラスをデフォルトで提供していますが、このクラスはcount(項目数の合計)、next(次のページネーションへのリンク)、previous(前のページネーションへのリンク)の3つの情報のみを返すように実装されています。これでは情報として足りないので、PageNumeberPaginationを継承したクラスを定義します。

次に、このページネーション用のクラスを使うことをviewの中で宣言します。pagination_classに上に実装したクラスを設定します。

これにより、バックエンドからは以下のような結果が返ってきます。

これらの情報を使って、フロントエンド側では下図のように情報を紐付けます。

フロントエンドでの情報の紐付け

7.3.8 ファイルをアップロードしたい

ファイルアップロードの仕組み

 通常のjson形式は、その値として数値か文字列のみを扱うため、バイナリーデータを送信することはできません。(どうしてもjsonで送信したい場合は、Base64などのバイナリー/テキスト変換を使ってバイナリファイルの内容をテキストに変換して送信するという方法はありますが、非常に大きなデータになってしまうためあまり実用的ではありません。)

バイナリファイルを送信するためには、バイナリをその他の型のデータと一緒に送信する「Form」という仕組みを使うのが一般的です。「FORM」とは、入力・送信フォームを作成する際に使用するHTMLの要素です。例えば、フォームに入力されたのが

 caption : This is a sample text 
  picture : example.jpg

という内容であれば、以下のようなHTTPリクエストとして送信されます。

まず、ヘッダーのContent-Typeには、multipart/form-dataが指定されます。これは、bodyに入るのが「Formデータ」であり、「マルチパート」に分割して送信するという意味です。分割を行うために、boundaryとして使う文字列が指定されます。

boundary文字列で分割された最初の領域には、’caption’属性の値’This is a sample text ’がテキスト形式で入っています。次のパートには、’picture”属性の値に指定されたファイルのファイル名がfilenameに入り、ファイルの内容がその下に格納されます。

このHTTPリクエストを受け取ったサーバー側でファイルを復元する処理を行うことで、ファイルアップロードの処理が完了します。

DRFでのファイルアップロードの実装

 DRFを使うと、このサーバー側の処理は、非常に簡単に記述できます。

(1)設定 アップロードファイルを格納する領域をsettings.pyに設定します。

(2)モデル

(3)シリアライザー

(4)View


Content-Type=“multipart/form-data”を持つHTTPリクエストを受け取ると、DRFはマルチパートのリクエストボディーを読み取って、ファイルをメモリに一時的に保存します。

ViewがSerializer.save()を呼ぶと、内部でモデルのインスタンスが作成されます。モデルの pictureFileField)にファイルオブジェクトが渡されると、Djangoは自動的に「ストレージへの書き込み」と「パスの生成」を行います。

テスト方法

 以下のように、curlを使ってテストをすることができます。

Formを送信する時は、ーFオプションを使います。ただ複数の属性を一度に定義できないので、一つづつ-Fで指定することになります。

 また、curlでPOSTの試験をする時は、一時的に下のようにmethod_decoratorをviewのクラス定義の前に記述してください。