7.2 Viewを作るための仕組み ~なぜDjango+DRFではコーディングが簡単なのか?~

Viewのプログラミングを容易にするためにDjangoやDRFによって提供されるのは、主に以下の3つです。

  • データベースへのアクセスを簡単に行う仕組み
  • JSON形式のデータを簡単に扱う仕組み
  • Viewを作るための一般的な型(パターン)

以下、これらの仕組みを大まかに理解するために、ポイントだけを説明をします。

7.2.1 データベースへのアクセスを簡単に行う仕組み(ModelとQueryset)  ~SQL不要のコーディング~

 Djangoはデータベースへのアクセスを簡単に行うために、O/Rマッピングの機能を提供しています。O/R(Object/RDB)マッピングとは、オブジェクト指向プログラミング言語においてリレーショナルデータベースのレコードを通常のオブジェクトとして操作する方法のことです。

 リレーショナルデータベースからデータを取り出したり、データを更新したりするためには、結構煩わしい手続きが必要です。リレーショナルデータベースにはSQL言語という専用言語を使ってアクセスし、さらに取り出したデータを新たに生成したオブジェクトの中に格納する処理が必要になります。O/Rマッピングは、このような手続きを隠蔽しオブジェクトの操作のみでリレーショナルデータベースへの書き込みや更新ができるようにしたものです。

O/R マッピング

 データベースへのアクセスは基本的にmodels.pyに記載される「モデル」を介して行われ、リレーショナルデータベースの中身の変更などはモデルの操作により自動的に行われます。例えば、学生名簿を表すモデルを定義してみます。

 各モデルはdjango.db.models.Modelのサブクラスとして定義されます。各モデルは単一のデータベースのテーブルに対応づけられ、モデルの属性はそれぞれがテーブルのフィールドを表します。
(実際には、モデルを定義した後にdjangoのコマンドラインユーティリティであるmanage.pyを使ってデータベーステーブルを生成するのですが、
これについては、7.3で述べます。)

テーブルに対して処理を行う方法は2つあります。

  • モデルのインスタンスを直接操作する方法
  • QuerySetを使う方法

通常、インスタンスを直接操作する方法を使う機会はあまりなく、ほとんどの場合QuerySetを使うのですがここでは両方紹介しておきます。

7.2.1.1 モデルのインスタンスを直接操作する方法

データの追加、アップデート、削除についてはこの方法が使えます

(1)データの追加

1〜2行では Departmentクラスのインスタンスを作り、インスタンスのsave()メソッドを呼ぶとデータベースに反映されます。3行目は、同じようにStudentクラスのインスタンスを作りますが、この時ForeignKeyでDepartmentとのリレーションが設定されているdepartmentの引数にはDepartmentクラスのインスタンス(この場合はd1)を設定します。

(2)データをアップデートする

インスタンスの属性に値をセットし、save()メソッドを呼ぶとデータベースにアップデートが反映されます。

(3)データの削除

インスタンスのdelete()メソッドを呼ぶとデータベースからデータが削除されます。

 直接操作をする方法は、非常に直感的でわかりやすいデータアクセス方法ですが、この方法ではテーブルからデータを検索して取得することができません。データの取得は次で述べるQuerySetを使って行います。

7.2.1.2 QuerySetを使う方法

1)QuerySetとは

 QuerySetはDjangoが提供するデータを取得手段です。QuerySetは以下の2つの側面を持つオブジェクトです。

  • データベーステーブル及びテーブルから取得したデータ集合に対するAPIを提供する: データテーブルから条件に合ったものを取り出すAPIを提供します。取り出したデータ集合に対してさらに条件によるフィルタリングを行うこともできます。さらにデータテーブルにデータの追加、変更、削除を行うAPIも提供します。
  • データベースから取得したオブジェクトの集合を保持する: QuerySetはデータベースアクセスなどの結果得られたオブジェクトの集合を保持しています。QuerySet自体がIterableな型になっており、リストや集合などと同じように、for文やwhile文で扱うことが可能です。

 通常のSQLによるデータベースアクセスでは、「データベースからどのようなデータをとってくるかを記述して実行するSQLQuery」と「Queryによって得られたオブジェクトであるResultSet」は別物になっていますが、Djangoでは、この2つの役割を「QuerySet」という一つのオブジェクトが担っています。(この辺りが、通常のデータベースプログラミングに慣れた人にとっては、わかりにくく違和感があるところです。)

(2)QuerySetの使い方
(2ー1) 複数のデータを取得する

  前に定義したテーブルのStudentテーブルのデータ全てを取得する場合は以下のようになります。

 これはSQL文、SELECT * FROM student に相当します。

次に、ある条件に該当するデータ全てを取得するQuerySetです。Studentテーブルの中の1年生だけのデータを取得する場合は以下のようになります。

 これは、SQL文、SELECT * FROM student WHERE grade=1 に相当します。

さらに他の条件を連結することもできます。Studentテーブルの中の1年生だけのデータを取得し、さらに名前がTaroのデータを除外したい場合は以下のようになります。

上の例では、一見APIが呼ばれデータベースから取ってきたデータがqueryset変数に格納されるように見えますが、実際はquerysetという変数にどのようにデータを取得するのかを設定しているだけで、実際にデータベースのアクセスは行われません。では、どのようにしてデータベースからのデータ取得をするのでしょう?

実は、QuerySetは「遅延評価」されるオブジェクトなのです。QuerySetを作った時点ではデータベースへの操作は行われず、QuerySetが評価(参照)される時に初めてデータベースアクセスが行なわれます。

この場合、1行目ではデータベース操作は行われません。2行目のprintの引数としてquerysetが評価される時に初めてデータベースへのアクセスが行われ、querysetには結果がセットされます。具体的には、複数のモデルオブジェクトを格納したQuerySetオブジェクトがセットされます。QuerySetはIterable(繰り返し処理が可能なオブジェクト)なので下のようにfor文などで扱うことができます。

実行結果は以下のようになります。

(2-2) データを一つだけ取得する

クエリーにマッチするのは1つのオブジェクトだけだと分かっている場合は、get()を使えばそのオブジェクトを取得することができます。

上の例の場合、「pk」はStudentテーブルのプライマリーキーを表しており、一意に決まるプライマリーキーでオブジェクトを指定していますので、マッチするオブジェクトは1つだけになります。このように。得られる結果がオブジェクトの場合は、即時評価が行われます。

(2-3)データの追加、アップデート、削除

これらの場合も即時評価が行われます。

(3) QuerySetの仕組み

 QuerySetがどのような仕組みで動くかを少し解説しておきます。

 モデルクラスであるStudentは、クラス変数objectManagerというモデルに対するデータベースクエリの操作を提供するインターフェイスを持っています。ManagerQuerySetクラスを継承しており、all()filter()get()などのQuerySet APIを持ちます。(下記参照)

このインタフェースを実行すると、その結果が得られますがこの結果もやはりQuerySetです。このため、この結果に対してさらにQuerySet APIを実行することができます。

QuerySetの仕組み

主なインタフェース説明
filter指定した条件に一致するオブジェクトのみを含むクエリセットを返す
exclude指定した条件に一致しないオブジェクトを含むクエリセットを返す
annotate各オブジェクトに集計・計算関数によって計算したデータを付加する。新しいフィールドをクエリセットに追加
allモデルの全てのオブジェクトを取得する
get指定した条件に一致する1つのオブジェクトを返す
create新しいオブジェクトを作成し、データベースに保存
updateクエリセット内のオブジェクトを一括で更新
deleteクエリセット内のオブジェクトを削除
existクエリセットに少なくとも1つのオブジェクトが存在するかどうかをの判定結果を返す
countクエリセットに含まれるオブジェクトの数を取得
QuerySetが提供する主なインターフェース

7.2.2 JSON形式のデータを簡単に扱う仕組み (Request  + Response + Serializer)

 DRFはRequest, Response,Serializerの3つのクラスを使って、JSON形式のデータをモデルインスタンスに変換したり、逆にインスタンスをJSON形式のデータに変換したりする仕組みを提供します。

  • Request : Webブラウザから送られネットワーク経由でWebサーバに渡されたHTTPリクエストの情報がインスタンス化されたもので、このオブジェクトがViewに渡されます。
  • Response : Requestとは逆に、WebサーバからWebブラウザに渡すHTTPレスポンスの情報をインスタンス化するもので、Viewの中でこれを作って返します。
  • Serializer : モデルインスタンスをPythonの辞書型のデータに変換したり、逆に辞書型のデータからインスタンスを作ったりすることができるコンバーターです。インスタンスから辞書型データへの変換をしリアライズ、辞書型データからインスタンスへの変換をデシリアライズと言います。

これら3つを組合せることで、やり取りされるJSON型データをプログラムで簡単に扱えるようになります。

JSONデータ変換の仕組み

(1)シリアライズする

 シリアライズの仕組みは主に、Webブラウザにレスポンスを返す時に使われます。
まず、普通のインスタンスをシリアライズする方法を見てみます。例として、商品の情報をまとめた、次のようなProductクラスを使うことにします。

このクラスのインスタンスをどのように変換を行うかを、serializers.Serializerクラスから継承したクラスに定義します。

 こんな感じで、インスタンスが持つ各フィールド名に対して、その型に対応したDRFが提供するFieldクラスを定義します。定義したシリアライザーを使って実際にシリアライズを行うコードが以下です。

この結果、ResponseされるJSONデータは以下のようになります。

 次に、モデルインスタンスをシリアライズする方法です。DRFのviewでは、データベースから取ってくる値はModelインスタンスとして扱うため、普通のインスタンスのシリアライズよりもこちらの方が圧倒的によく使われます。

7.2.1 で使ったStudentモデルの例で説明します。もう一度Studentモデルを見てみましょう。

見ての通り、Studentクラスのモデルを定義する段階で、各フィールド名に対してすでにFieldクラスを定義しています。普通のインスタンスのシリアライザーと同じように

のように定義することもできますが、モデルで定義した情報と重複した情報を、シリアライザークラスでも定義するのは冗長です。もっと簡単に定義するためにserializers.ModelSerializerと言うクラスが提供されており、以下のように定義することができます。

 StudentSerializerのインナークラスとしてMetaと言う名前のクラスを定義しこの中でmodelfieldsを定義します。modelには、シリアライザーの対象とするモデルのクラス名を記載します。fieldsには、モデルクラスで定義したフィールドのうち、シリアライズの対象とするフィールド名をタプル形式もしくはリスト形式で記述します。このように定義することで、モデルシリアライザーは、モデルに基づいたフィールドを自動的に生成します。

 定義したシリアライザーを使った実際のコードが以下です。querysetでデータベースから取ってきた値をシリアライズしている例です。

  • 1行目で定義したquerysetを2行目のStudentSerializerのコンストラクターの第一引数にセットすることで、querysetがこのシリアライザーの入力になります。第二引数のmany=Trueは、入力されるデータが複数あることをシリアライザーのコンストラクターに伝えます。
  • 2行目が実行される時点でquerysetによるデータ取得が行われ、これを入力としたシリアライズが行われます。シリアライズされた結果は、serializer.dataに格納されます。
  • 3行目ではこれをResonseに引数として渡すことで、シリアライズされたデータを含むHTTPレスポンスを作り、これを返しています。

この結果、ResponseされるJSONデータは以下のようになります。

モデルで定義したフィールド全てをシリアライズ対象にする場合には、タプルの代わりに’__all__’と記載することもできます。

これを使ってシリアライズした結果は、次のようになります。

ForeignKeyフィールドが指定されているdepartmentには、指定されたDepartmentインスタンスに割り振られたキー値が入ります。

(2)デシリアライズする

 デシリアライズの仕組みは、主にPOSTメソッドやPUTメソッドのHTTPリクエストによって送られたデータをデータベースに格納したり値をアップデートしたりする際に、送られてきたデータをモデルインスタンスに変換するために使われます。そのため、普通serializer.ModelSerializerを継承したシリアライザーを使い、シリアライズをする時に定義したものをデシリアライズの場合でもそのまま使います。

 デシリアライズの際の動作はシリアライズの場合に比べ、以下のように少し複雑です。

  • データベースにデータを追加するかアップデートするかで、使われ方が異なります
  • モデルインスタンスに変換する前に入力されたデータがインスタンス化できるかどうかの評価を行う必要があります(例えば、IntegerFieldの属性の値に文字列が入っていた場合にエラーを発生させたりします。)
  • データをインスタンス化するだけではなく、そのデータをデータベースへ格納するところまで行います。(ModelSerializerの場合のみ)

 例を使って説明します。POSTリクエストがviewに届くとpostメソッドが呼び出されrequestが渡されます。

  • 2行目では、requestに含まれている辞書形式のデータをdata=request.dataでStudentSerializerのコンストラクターに入力として渡しています。
  • 3行目では、入力されたデータがインスタンス化できるかどうかの評価を行っています。インスタンス化できない場合はここでExceptionが発生します。また、この評価が成功しないとインスタンスへの変換は行われません。
  • 4行目で、実際にモデルインスタンスが生成され、データベースに値が保存されます。

is_validメソッドとsaveメソッドの中で行われる処理は、serializers.ModelSerializerクラスの中で実装されていますが、これを変更したい場合には、is_validメソッドやsaveメソッドから呼ばれるcreateおよびupdateメソッドをStudentSerializerクラスの中で独自に実装します。

7.2.3 Viewを作るための一般的な型

 DjangoやDRFはフレームワークであり、文字通り型にはまったViewの作り方を提供します。Viewを作るための型(パターン)がいくつか提供されており、このパターンに従って作ると非常に少ないコードでプログラムが出来上がります。

(1)Generic API View

 データに対する基本的な操作には、データの生成(Create)、読み取り(Read)、更新(Update)、削除(Delete)の4つがあり、頭文字をとって「CRUD」と呼ばれています。CRUDそれぞれについてViewプログラムの雛形が用意されており、これはGeneric API Viewと呼ばれます。読み取り(Read)については、一覧データを取得する場合と、単一データを取得する場合の二種類があり、雛形は全部で5パターンになります。

  • 一覧データを取得する  ListAPIView list
  • 単一のデータを取得する  RetrieveAPIView retrieve
  • データを追加する  CreateAPIView create
  • データを変更する  UpdateAPIView update
  • データを削除する  DestroyAPIView destroy

例えば、一覧データを取得するためのViewとしては「ListAPIVIew」として予め準備されています。DRFのコードをみると、ListAPIVIewの中で行われる処理は、デフォルトでは以下のようになっています。

中間のpage = 以下の4行は、後述するページネーションの機能を使うためのコードで、この部分がなくても動きますので、一旦無視してください。

  • 2行目で、querysetが設定されています。この行では、self.querysetという変数からquerysetが呼び出されセットされます。
  • 9行目では、querysetを渡してシリアライザーをインスタンス化しています。
  • 10行目では、シリアライズされた結果が格納されたserializer.dataをResponseに渡され、JASON形式でデータが返送されます。

残りの4つのパターンの処理コードはここでは紹介しませんが、ListAPIViewも含め図にすると以下のようになります。

Generic API Viewの内部処理

 上記で示した処理のうち、retrieve, update, destroyはHTTPリクエストの中でプライマリーキー値を指定して、特定のモデルオブジェクトを取り出して処理を行います。モデルオブジェクトの取り出しはget_object()というメソッドで行います。設定されているqueryset.get(pk=<キー値>)を接続してデータ呼び出しを行います。例えばquerysetStudent.objects.all()であれば、Student.objects.all().get(pk=1)でデータを呼び出し、特定のオブジェクトを取り出します。

このように、データベースから取得値をそのまま返したり、送信した値をそのままデータベースに格納したりするのであれば、そのための処理は予め準備されており、新たに何かをコーディングする必要はありません。独自に必要なのは、querysetserializerを定義することだけです。つまり、ユーザは次のようなコード2行だけを書けば良いことになります。

 前のlistのコードの中でserializer_classself.get_serializer()の中で、querysetはself.get_queryset()の中で呼び出され実行されます。querysetの中で、requestに含まれるパラメータを使ってデータを取得したい場合は以下のようなコードになります。

いずれにしても、あらかじめ用意された定型的な処理を行うだけであれば、非常に少ないコード量でプログラムが完成します。

(2)View Set

 一つのモデルに対するCRUDを操作するviewをひとまとめにして実装する方法を提供するのがModel Vies Setです。上のGeneric API Viewを使う場合、一つのモデルへのCRUDそれぞれに対してViewを実装する必要がありますがほとんどの場合、CRUDそれぞれで使うquerysetとSerializerは一緒になるため、同じようなコードをそれぞれの viewに書くことになってしまいます。

 DRFはCRUDの処理を一つのクラスにまとめた「ViewSet」という仕組みを提供しており、これを使うとCRUDのそれぞれのViewを定義する必要がなくなります。ModelViewSetを使うと、こうなります。

 単にStudentモデルを扱うだけであれば、ViewSetクラスにはlist, retrieve, create, update, destroyのデフォルトメソッドが全て実装されているので、serializerquerysetを定義するだけです。

ModelViewSetのディスパッッチャーでの設定の仕方は、通常のViewとは異なりrouterという仕組みを使います。

上のように定義すると、自動的に以下のような呼び出し関係が設定されます。

  • GET /students/  StudentViewSetのlist()呼び出し
  • POST /students/  create() 呼び出し
  • GET /students/<pk>/ retrieve() 呼び出し
  • PUT /students/<pk>/ update() 呼び出し
  • DELETE /students/<pk>/ destroy() 呼び出し

(3)Generic APIView vs. View Set     どちらを使うべきなのか?

 ViewSetは少ないコード量でCRUD全ての処理を実現できますが、だからと言って全ての場合にView Setを使うべきではありません。あるモデルに対して一覧表示のAPIのみが必要な場合はGeneric API Viewを使うべきです。ViewSetを使うとlistだけでなく本来不要なはずのcreateやupdateやdeleteのAPIを開放してしまうことになり、これらがセキュリティーホールに成りかねないからです。