【Django】MIDDLEWAREを作って、常にデータを表示する【requestにモデルオブジェクトを属性として追加する】
例えば、サイトのどのページにアクセスしても表示させたいデータがあるとしよう。サイトのトップバーあたりにサイトの更新履歴(ニュース)のようなものをスライドして表示させたい時などがそうだ。サイトのニュースだから当然、誰でも見れる仕組みにする。
こういうどのページでも常時表示しなければならないデータがある時、いかにして手を打つか。当然、更新履歴は管理サイトから追加していくものだから、モデルを参照しなければならないものとする。
すぐに思いつく方法論(いずれも却下)
御託だと思う場合は次項へ。すぐに思いつく方法として以下のものが上げられる
- カスタムテンプレートタグを使用する
- 全てのビューでモデルオブジェクトを作ってレンダリング
- ニュースのモデルオブジェクトを属性値としたビュークラスを作って継承する
- カスタムユーザーモデルと紐付けて参照する
カスタムテンプレートタグを使用する
カスタムテンプレートタグを実行。テンプレート側でモデルにアクセスしてニュースのデータを抜き取り、レンダリングさせる。おそらく埋め込み型テンプレートタグになるだろう。またテンプレートが増える。
実装はそれほど難しくはないが、テンプレート上でモデルにアクセスするという逆流現象に違和感があるような気がする。
カスタムテンプレートタグが実行されるのはクライアントにレスポンスが返却される寸前であり、その後に追加でできる事は限られる。個人的にはカスタムテンプレートタグに頼るのは最終手段だと考えているため、今回は却下とした。
全てのビューでモデルオブジェクトを作ってレンダリング
ゴリ押し戦法。存在する全てのビューに、ニュースのモデルオブジェクトを作り、コンテキストに値を与えてレンダリングする。
しかし、この方法は非常に危険。ビューが複数ある場合、ひとつでもその処理が抜け落ちてしまうとニュースエリアは表示されない。
それに、同じ処理が全てのビューの中に書かれている様は、あまり美しいコードとは言えないだろう。よってこの方法は却下とした。
ニュースのモデルオブジェクトを属性値としたビュークラスを作って継承する
つまりこういうこと。
#前略
class BaseView(View):
news = News.objects.all()
class XXXView(BaseView):
def get(self,request,*args,**kwargs):
context = { "news":self.news }
#以下略
一見うまく行きそうだが、newsに入るのはサーバーを起動したときのNewsに格納されてあるデータ。つまり、サーバーを起動した後、BaseViewではNewsのモデルオブジェクトを作ってnews属性とする。その後、サーバーを再起動でもしない限り、二度と更新される事はない。
サーバーを起動した後、管理サイトでニュースを追加しても、サイト上では追加されたことにならないのだ。
また属性であれメソッドであれ、contextに値を入れる必要がある。この時点で前項のビュー全てにモデルオブジェクトを作ってレンダリングする方法と大して変わらない。
カスタムユーザーモデルと紐付けて参照する
カスタムユーザーモデルとニュースのモデルを1対多、多対多のリレーションで紐付けると、テンプレートタグから{{ request.user.xxx }}
などと参照できる。これは良いと思う。
しかし、問題はユーザーモデルと紐付いているため、ログインしているユーザーしか参照できない。つまり、サイトのニュースは未ログインのユーザーに対しても見せたいのに、ログイン済みのユーザーにしか見せることができない。
そして、ユーザーとニュースのモデルをそれぞれいかにして紐付けるかという問題が発生する。ユーザーに対応した既読判定用のフィールドをつけるならともかく、意味のないリレーションを組むのは厳禁である。よってこの方法も却下とした。
MIDDLEWAREを作成して、リクエストオブジェクトの属性にモデルオブジェクトを値として代入する。
ミドルウェアを作る。後は前回の記事に倣ってsettings.py
のMIDDLEWARE
に追加する。
from django.conf import settings
from ..models import News
import datetime
class AddNewsAttribute:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
url = request.get_full_path()
if request.method == "GET" and settings.MEDIA_URL not in url and settings.STATIC_URL not in url:
today = datetime.date.today()
request.NEWS = News.objects.filter( start_date__lte=today, end_date__gte=today).order_by("-dt")
print("Newsモデルオブジェクトを属性値として付与")
response = self.get_response(request)
return response
.get_full_path()
を使ってURLを取得。MEDIA_URL
とSTATIC_URL
以外のURLであり、リクエストのメソッドがGETであるものに対してモデルオブジェクトを生成して属性として付与する。
後は、テンプレート側で{{ request.NEWS }}
などと呼び出す。
問題点
前回のMIDDLEWAREの記事と同様にMIDDLEWAREは必ず通る。どんなリクエストであっても必ず通る。
故に処理速度が気になる。前回はメディアファイルのアクセスを拒否して即レスポンスという処理であり、場合によっては処理を軽減できる可能性もあるが、今回は単純に追加されただけである。
特にリクエストが送信される度、条件式がTrueになれば(というかほぼ全てのリクエストで)、必ずDBへのアクセスが発生する。ここはキャッシュか何かでDBへの負担を軽減したいところだが、具体的な方法が見つからなかったため、今回は見送り、このままとした。キャッシュを使用してのDBアクセス負担の軽減に関しては分かり次第、記事にまとめる。
結論
ここまで込み入ってくると、どんな方法でも一長一短というケースが多い。しかし、MIDDLEWAREは使いようによってはまだまだ化けるのではないかと思う。
とは言え、個人の好みや開発のスタイルとかを考慮した上で実装するのが妥当かと。