最近、Djangoを使って仕事しています。
バージョン
| 種類 | バージョン |
|---|---|
| Python | 3.7.0 |
| pip | 18.1 |
| django-debug-toolbar | 1.10.1 |
最近起こったこと
検索フォームを用意しました。
その一部に選択欄を用意するために、独自フォームも用意しました。
画面はこんな感じです。

モデル
対象モデルは以下の通りです。
- 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.pyのINSTALLED_APPSにdebug_toolbarを追加します。
INSTALLED_APPS = [
# 省略
'django.contrib.staticfiles',
# 省略
'debug_toolbar',
]
デフォルトからあるものをいじっていなければ、debug_toolbarのみ追加すればよいです。
続いて、MIDDLEWAREにdebug_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
これで動くはずです。
動かしてみる
同じ画面を開きます。

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

入っているデータ量と構造に依存しますが、今回は同じようなクエリが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')
修正後

クエリ呼び出し回数:167回→7回
クエリ実行時間:154.43ms→8.89ms
と、大幅に減少したことがわかります。ページ描画時間も「3851.68ms→385.52ms」と、大幅に短縮しました。 描画中に何度も問い合わせしていたのかもしれません。
メリット
django-debug-toolbarの良いところは、ソース修正してすぐに、クエリ等の状態を確かめられる点だと感じました。
これがないと、何となくソースを修正して、早くなった気がするという感じの、感覚的な修正になっていたと思います。
それを、デバッグ実行しただけで、そのサイト上からクエリ確認ができるのは、すごく楽です。
おわりに
Djangoについては、日本語記事がそれほど多くない印象です。
django-debug-toolbarについても、インストール手順等書いてある記事がありましたが、結局英語サイトから引っ張りました。
英語の公式サイト見ないと、設定等はしんどい部分はあります。
それでも、Django自体、かなり使いやすいので、これから仕事以外でも使っていこうと思っています。
…ようやく、Webへの苦手意識が取れてきた感じです。
今日はdjango-rest-frameworkやらTypeScriptやら使っていて、なんとか突破できた感じでしたし、そこで学んだことがまとまったら、書いていきます。
ではでは。