メモ帳

python, juliaで機械学習をやっていく

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へのカウントアップを効率的に行うことができます。 頻出の処理なのでぜひ使って見て下さい!