F関数を使ってDjangoで効率よくquerysetのカウントアップを行う
検索ヒット回数や、QAのフィードバックのためにDB内の値をカウントアップしたいとき、単純にDjangoのquerysetを使うと、1. 値の取得 2. 値の更新の合計2回DBにアクセスしなければいけません。しかし、F関数を使えば1回のアクセスで更新できます。
model
models.pyに以下のように定義されているとします。 countup/models.py
from django.db import models class FAQ(models.Model): question = models.CharField("question", max_length=300) answer = models.CharField("answer", max_length=500) useful = models.PositiveIntegerField("the number of access", default="0") def __str__(self): return self.question
このFAQモデルの中からアクセスされたIDのusefulカラムの値を1ずつカウントアップしたいと思います。
F関数
以下のように、F関数の引数にカラム名を指定すると、DB内の値を、pythonが受け取らずに直接DB側で参照し値の更新をしてくれます。
from django.db.models import F from .models import FAQ # F関数未使用 faq = FAQ.objects.get(pk=pk) faq.useful = faq.useful + 1 faq.save() # F関数使用 FAQ.objects.filter(pk=pk).update(useful=F('useful') + 1)
viewとtemplateの実装例
挙動の確認を行うためにviewとtemplateを実装します。(確認用なのでtemplateはとんでもなく簡素です) view, template, modelはcountup/以下に配置しています。
views.py
FAQ一覧表示用のviewをListViewを継承して定義し、カウントアップ用のviewを関数ベースで作成します。 カウントアップ用のviewは処理が終わったら一覧表示のページにリダイレクトすることにします。 countup/views.py
from django.http import HttpResponseRedirect from django.shortcuts import reverse from django.views.generic.list import ListView from .models import FAQ from django.db.models import F # Create your views here. class FAQListView(ListView): """FAQの一覧表示(usefulのカウント増加の確認用)""" model = FAQ template_name = "countup/list.html" def update_useful(request, pk): """指定されたpkのFAQのusefulを1増やして、一覧表示ページにリダイレクトする""" # F関数使用 FAQ.objects.filter(pk=pk).update(useful=F('useful') + 1) # F関数未使用 # faq = FAQ.objects.get(pk=pk) # faq.useful = faq.useful + 1 # faq.save() success_url = reverse("countup:list") return HttpResponseRedirect(success_url)
urls.py
2つのviewのpathを書きます。 main/urls.py
from django.contrib import admin from django.urls import path, include import countup.urls urlpatterns = [ path('', include(countup.urls, namespace="countup")), path("admin/", admin.site.urls) ]
countup/urls.py
from django.urls import path from .views import FAQListView, update_useful app_name = 'countup' urlpatterns = [ path('', FAQListView.as_view(), name="list"), path('<int:pk>/', update_useful, name="countup"), ]
template
FAQ一覧表示用のHTMLは以下になります。update_useful関数はFAQListViewにリダイレクトするのでtemplateを用意する必要はありません。 countup/list.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>countup sample</title> </head> <body> {% for obj in object_list %} <a href="{% url 'countup:countup' pk=obj.id%}">カウントアップ</a> <p>question: {{ obj.question }}</p> <p>answer: {{ obj.answer }}</p> <p>useful: {{ obj.useful }}</p> {% endfor %} </body> </html>
確認
DBアクセスのログを表示して実際の処理を確認してみます。
DBアクセスのログを表示
settings.pyに以下を追加
LOGGING_CONFIG = None LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'all': { 'format': '\t'.join([ "message:%(message)s", ]) }, }, 'handlers': { 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'all', }, }, 'loggers': { 'logger': { 'handlers': ['console'], 'level': 'DEBUG', 'propagate': True, }, 'django.db.backends': { 'handlers': ['console'], 'level': 'DEBUG', }, }, } logging.config.dictConfig(LOGGING)
出力
F関数未使用
>>> message:(0.001) SELECT "countup_faq"."id", "countup_faq"."question", "countup_faq"."answer", "countup_faq"."useful" FROM "countup_faq" WHERE "countup_faq"."id" = 1; args=(1,) >>> message:(0.000) BEGIN; args=None >>> message:(0.006) UPDATE "countup_faq" SET "question" = 'テスト質問1', "answer" = 'テスト回答1', "useful" = 7 WHERE "countup_faq"."id" = 1; args=('テスト質問1', 'テスト回答1', 7, 1) >>> message:(0.000) SELECT "countup_faq"."id", "countup_faq"."question", "countup_faq"."answer", "countup_faq"."useful" FROM "countup_faq"; args=()
F関数を使用
現在の値を取得するselectひとつ分DBへのアクセスが少ないことが分かります。
>>> message:(0.000) BEGIN; args=None >>> message:(0.001) UPDATE "countup_faq" SET "useful" = ("countup_faq"."useful" + 1) WHERE "countup_faq"."id" = 1; args=(1, 1) >>> message:(0.001) SELECT "countup_faq"."id", "countup_faq"."question", "countup_faq"."answer", "countup_faq"."useful" FROM "countup_faq"; args=()
このようにしてF関数でDBへのカウントアップを効率的に行うことができます。 頻出の処理なのでぜひ使って見て下さい!