SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

Djangoのdjango-debug-toolbarが便利だった件

最近、Djangoを使って仕事しています。

バージョン

種類 バージョン
Python 3.7.0
pip 18.1
django-debug-toolbar 1.10.1

最近起こったこと

検索フォームを用意しました。

その一部に選択欄を用意するために、独自フォームも用意しました。

画面はこんな感じです。

f:id:hiroronn:20181016085207p:plain

モデル

対象モデルは以下の通りです。

  • models.py
class Target(models.Model):
    """測定項目"""
    id = models.BigAutoField(primary_key=True)
    parent = models.ForeignKey('Target', on_delete=models.CASCADE, null=True, blank=True)
    name = models.CharField(max_length=100)

    @property
    def hierarchy_name(self):
        res = self.name
        p = self.parent
        while p:
            res  = p.name + " - " + res
            p = p.parent
        return res

自身の別項目を親とする再帰テーブルです。
自身の名称を、親まで含めて取得するプロパティを持っています。

大項目 - 中項目 - 自身

のような形で表示するためのプロパティです。

フォーム

検索として指定された値を入力するためのフォームです。

  • forms.py
class SearchForm(forms.Form):
    """検索フォーム"""
    target = forms.ChoiceField(choices=(), label='対象の評価値')
    expression = forms.ChoiceField(choices=(('eq', '一致する'), ('neq', '一致しない'), ('gt', 'より大きい'), ('lt', 'より小さい')), label='条件')
    search_text = forms.CharField(label='検索値')

    def __init__(self, **kwargs):
        super(SearchForm, self).__init__(**kwargs)

        parents = Target.objects.values_list('parent', flat=True).distinct()
        p = [x for x in parents if x]
        items = Target.objects.exclude(id__in=p)

        self.fields['target'].choices = [(x.id, x.hierarchy_name) for x in items]

選択できるのは末端の項目だけなので、

parents = Target.objects.values_list('parent', flat=True).distinct()
p = [x for x in parents if x]

で、まずは親として指定されているIDだけを抽出し、

items = Target.objects.exclude(id__in=p)

で、親となった項目以外Targetを抽出しています。

結果

遅い、です。

最終行で

self.fields['target'].choices = [(x.id, x.hierarchy_name) for x in items]

とやっていますが、ここで親の名前を取得するために、二回ずつクエリが発行されているのだろうと思っていました。

しかし、その証拠がないし、調べるのも手間だと思っていた、というのが背景です。

django-debug-toolbarを入れる

Installation — Django Debug Toolbar 1.10.1 documentation

上記リンクに書いてありますが、説明します。

pip

pip install django-debug-toolbar

settings.py

サイトのsettings.pyINSTALLED_APPSdebug_toolbarを追加します。

INSTALLED_APPS = [
    # 省略
    'django.contrib.staticfiles',
    # 省略
    'debug_toolbar',
]

デフォルトからあるものをいじっていなければ、debug_toolbarのみ追加すればよいです。

続いて、MIDDLEWAREdebug_toolbar.middleware.DebugToolbarMiddlewareを追加します。

MIDDLEWARE = [
    # 省略
    'debug_toolbar.middleware.DebugToolbarMiddleware',
]

最後に、末尾でよいので以下を追加します。

INTERNAL_IPS = ['127.0.0.1']

自身の環境のデバッグ時のみ有効なIPにします。

URLconf

ルートサイトのurls.pyに、以下を追加します。

from django.conf import settings # 追加
from django.urls import path, include

 # 以下、末尾に追加
if settings.DEBUG:
    import debug_toolbar
    urlpatterns = [
        path('__debug__/', include(debug_toolbar.urls)),
    ] + urlpatterns

これで動くはずです。

動かしてみる

同じ画面を開きます。

f:id:hiroronn:20181016202724p:plain

右側にツールバーが出てきます。

今回は、クエリのパフォーマンスが悪いのでは?とあたりを付けているので、見てみます。
ツールバーSQLをクリックします。

f:id:hiroronn:20181016203159p:plain

入っているデータ量と構造に依存しますが、今回は同じようなクエリが162回、まったく同じパラメーターで呼ばれたクエリが161回あると出てきます。

やはり非効率なようです。

修正してみる

このページを表示する際の処理が非効率なようなので、修正してみます。

フォーム作成後、何度も問い合わせが行われていることが原因のようなので、それを抑制します。

class SearchForm(forms.Form):
    """検索フォーム"""
    target = forms.ChoiceField(choices=(), label='対象の評価値')
    expression = forms.ChoiceField(choices=(('eq', '一致する'), ('neq', '一致しない'), ('gt', 'より大きい'), ('lt', 'より小さい')), label='条件')
    search_text = forms.CharField(label='検索値')

    def __init__(self, **kwargs):
        super(SearchForm, self).__init__(**kwargs)

        parents = Target.objects.values_list('parent', flat=True).distinct()
        p = [x for x in parents if x]
        items = Target.objects.exclude(id__in=p).prefetch_related('parent', 'parent__parent')

        self.fields['target'].choices = [(x.id, x.hierarchy_name) for x in items]

二回目の問い合わせで、

親となった項目以外Targetを抽出

している部分で、prefetch_relatedを呼び出し、先に値を取得してしまいます。

items = Target.objects.exclude(id__in=p).prefetch_related('parent', 'parent__parent')

修正後

f:id:hiroronn:20181016205522p:plain

クエリ呼び出し回数:167回→7回

クエリ実行時間:154.43ms→8.89ms

と、大幅に減少したことがわかります。ページ描画時間も「3851.68ms→385.52ms」と、大幅に短縮しました。 描画中に何度も問い合わせしていたのかもしれません。

メリット

django-debug-toolbarの良いところは、ソース修正してすぐに、クエリ等の状態を確かめられる点だと感じました。

これがないと、何となくソースを修正して、早くなった気がするという感じの、感覚的な修正になっていたと思います。

それを、デバッグ実行しただけで、そのサイト上からクエリ確認ができるのは、すごく楽です。

おわりに

Djangoについては、日本語記事がそれほど多くない印象です。

django-debug-toolbarについても、インストール手順等書いてある記事がありましたが、結局英語サイトから引っ張りました。
英語の公式サイト見ないと、設定等はしんどい部分はあります。

それでも、Django自体、かなり使いやすいので、これから仕事以外でも使っていこうと思っています。

…ようやく、Webへの苦手意識が取れてきた感じです。
今日はdjango-rest-frameworkやらTypeScriptやら使っていて、なんとか突破できた感じでしたし、そこで学んだことがまとまったら、書いていきます。

ではでは。