最近、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
やら使っていて、なんとか突破できた感じでしたし、そこで学んだことがまとまったら、書いていきます。
ではでは。