条件付きのビュー処理

HTTP クライアントは、さまざまな HTTPヘッダーを送信することで、受信したリソースについてサーバーに伝えることができます。この方法は、ウェブページを (GET メソッドを利用して) 取得する際に、すでにクライアントが取得しているデータを送信するのを回避するためによく使われます。しかし、同じヘッダーは、全ての HTTP メソッド (POSTPUTDELETE など) で利用できます。

Django がビューから送り返す各ページ (レスポンス) に対して、Django は2種類の HTTP ヘッダ、ETag ヘッダと Last-Modified ヘッダを提供できます。これらのヘッダは HTTP レスポンス上ではオプションです。ヘッダはビュー関数で設定することも、ConditionalGetMiddleware ミドルウェアを使用して ETag ヘッダを設定してもらうこともできます。

クライアントが次に同じリソースをリクエストしたとき、If-Modified-SinceIf-Unmodified-Since のような、最後に変更された時刻を含むヘッダ、あるいは、If-MatchIf-None-Match のような、最後に送信された ETag を含むヘッダとともにリクエストを送信することがあります。もしページの現在のバージョンがクライアントから送信された ETag と一致するか、リソースが変更されていない場合、完全なレスポンスの代わりに、ステータスコード304を送り返すことで、クライアントに何も変更がないことを伝えられます。ヘッダによりますが、もしページが変更されていなくて、クライアントから送られてきた ETag とも一致しない場合には、ステータスコード412 (Precondition Failed) が返せます。

より細かい粒度のコントロールが必要な場合には、ビューごとの条件付き処理関数を使えます。

condition デコレータ

時には(実際にはかなり頻繁に)、リソースの ETag 値や最終更新時間を迅速に計算する関数も作成できます。これは、完全なビューを構築するために必要なすべての計算を行う 必要がない 場合です。Django は、これらの関数を使用して、ビュー処理の「早期脱出」オプションを提供できます。たとえば、クライアントに対して、コンテンツが前回のリクエスト以降変更されていないことを伝えます。

これら 2 つの関数は django.views.decorators.http.condition デコレータのパラメータとして渡されます。このデコレータは、HTTP リクエストのヘッダがリソースのヘッダと一致するかどうかを調べるために、2 つの関数 (両方の量を簡単かつ迅速に計算できない場合は、1 つだけを渡す必要があります) を使用します。もし一致しなければ、リソースの新しいコピーを計算して通常のビューを呼び出します。

condition デコレータのシグネチャは次のようになります。

condition(etag_func=None, last_modified_func=None)

ETag と最終更新時間を計算するための2つの関数は、受信した request オブジェクトと、それらがサポートするビュー関数と同じパラメータを、同じ順序で渡されます。 last_modified_func に渡された関数は、リソースが最後に変更された時間を指定する標準の datetime 値を返すべきです。または、リソースが存在しない場合は None を返すべきです。 etag デコレータに渡された関数は、リソースの ETag を表す文字列を返すべきです。または、それが存在しない場合は None を返すべきです。

デコレータは、それらがビューによってまだ設定されていない場合、そしてリクエストのメソッドが安全なもの(GET または HEAD)である場合に、レスポンスに ETag ヘッダと Last-Modified ヘッダを設定します。

この機能を有効に使う方法は、例を挙げて説明するのが一番でしょう。小さなブログシステムを表す次のようなモデルのペアがあるとします。

import datetime
from django.db import models


class Blog(models.Model): ...


class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    published = models.DateTimeField(default=datetime.datetime.now)
    ...

最新のブログエントリーを表示するフロントページが、新しいブログエントリーを追加したときだけ変更されるのであれば、最終更新時刻をとても速く計算できます。そのブログに関連するすべてのエントリーの最新の published 日付が必要です。これを行う一つの方法は次のようなものです。

def latest_entry(request, blog_id):
    return Entry.objects.filter(blog=blog_id).latest("published").published

この機能を使えば、トップページビューで変更されていないページを早期に検出できます。

from django.views.decorators.http import condition


@condition(last_modified_func=latest_entry)
def front_page(request, blog_id): ...

デコレータの順番に注意

条件付きレスポンスが condition() によって返される場合、それ以下のデコレータはスキップされ、レスポンスに適用されません。したがって、通常のビューレスポンスと条件付きレスポンスの両方に適用する必要があるデコレータは condition() より上に置く必要があります。特に、 vary_on_cookie(), vary_on_headers(), cache_control() は、 RFC 9110 が 304 レスポンスにヘッダを設定することを要求しているため、最初に置くべきです。

1つの値を計算するだけのショートカット

一般的なルールとして、ETag と最終更新時刻の 両方 を計算する関数を提供できるのであれば、そうすべきです。どの HTTP クライアントがどのヘッダを送ってくるかはわからないので、両方を扱えるように準備しましょう。しかし、片方の値だけを計算するのが簡単な場合もあり、 Django は ETag だけ、あるいは last-modified だけを計算するデコレータを提供します。

django.views.decorators.http.etagdjango.views.decorators.http.last_modified デコレータは condition デコレータと同じ型の関数を渡されます。これらのシグネチャは次の通りです。

etag(etag_func)
last_modified(last_modified_func)

last-modified 関数だけを使う先ほどの例も、このデコレータを使って書くことができます。

@last_modified(latest_entry)
def front_page(request, blog_id): ...

もしくは、次のように書きます。

def front_page(request, blog_id): ...


front_page = last_modified(latest_entry)(front_page)

2つの条件をテストする場合に condition を使う

もし両方の前提条件をテストしたいのであれば、 etag デコレータと last_modified デコレータを連結させたほうがすっきりするかもしれません。しかし、これは正しくない動作につながります。

# Bad code. Don't do this!
@etag(etag_func)
@last_modified(last_modified_func)
def my_view(request): ...


# End of bad code.

最初のデコレータは 2 番目のデコレータについて何も知らないので、 2 番目のデコレータがレスポンスは変更されていると判断した場合でも、変更されていないと答えるかもしれません。条件 condition デコレータは両方のコールバック関数を同時に使用して、正しい動作を決定します。

その他の HTTP メソッドでデコレータを使用する

condition デコレータは GETHEAD リクエスト以外にも有用です (HEAD リクエストはこの状況では GET と同じです)。 POSTPUTDELETE リクエストのチェックにも使うことができます。このような状況では、"not modified" 応答を返すのではなく、変更しようとしているリソースがその間に変更されたことをクライアントに伝えることを考えます。

たとえば、つぎのようなクライアントとサーバを交換する例を考えてみてください。

  1. クライアントが /foo/ をリクエストします。
  2. サーバーは何らかのコンテンツを "abcd1234" という ETag を付けて返します。
  3. クライアントは HTTP PUT リクエストを /foo/ に送信して、リソースを更新します。If-Match: "abcd1234" ヘッダも送信して、更新しようとしているバージョンを指定します。
  4. サーバーは、GET リクエストと同じ方法で (同じ関数を使って) ETag を計算することで、リソースが変更されているかどうかをチェックします。もしリソースが すでに 変更されていた場合、「precondition failed」を意味するステータスコード412を返します。
  5. クライアントは、412レスポンスを受信した後、実際に更新する前に、更新されたバージョンのコンテンツを取得するために、GET リクエストを /foo/ に送信します。

この例が示している重要な点は、ETag と最終変更の値を計算するのに、すべての状況で同じ関数が使えるということです。実際、毎回同じ値が返されるように、同じ関数を使う 必要がある のです。

安全ではないリクエストメソッドを含むバリデーターヘッダ

condition デコレータは、安全な HTTP メソッド、つまり GETHEAD に対してのみバリデータヘッダ (ETagLast-Modified) を設定します。 それ以外の場合にこれらを返したい場合は、ビューで設定してください。 PUTPOST のリクエストに対するバリデーションヘッダの設定の違いについては RFC 9110#section-9.3.4 を参照してください。

ミドルウェアの条件付き処理との比較

Django は、django.middleware.http.ConditionalGetMiddleware 経由で条件付き GET 処理を提供しています。多くの状況に適していますが、ミドルウェアは高度な利用に次のような限界があります。

  • プロジェクト内のすべてのビューにグローバルに適用されてしまう。
  • レスポンスを生成する手間が省けるわけではなく、コストがかかる場合がある。
  • HTTP の GET リクエストに対してのみ適している。

ここでは、特定の問題に最も適したツールを選択するべきです。もし ETag と変更時刻を高速に計算する手段があり、一部のビューがコンテンツを生成するのに時間がかかるのであれば、このドキュメントで説明した condition デコレータを使用することを検討するべきです。もしすべてがすでにかなり高速に動作しているなら、引き続きミドルウェアを使用し続けてください。ビューが変更されていなければ、クライアントに送り返されるネットワークトラフィックは、引き続き削減されます。

Back to Top