DjangoでQuerySetを試しに組み立てる際にはdjango-extensionsのshell_plusを利用しよう

Djangoを利用するとき、QuerySetを利用してSQLを組み立てるケースが多いと思います。

しかし、QuerySetの経験に乏しいと、SQLがイメージできていても、どういうQuerySetを書けば良いか、というのがまだ判断できないケースも存在すると思います。

動作確認実施時に、決められた手順で作業をするものの、そのために何度も手順を繰り返すのも効率が悪かったりします。

そういう時に便利な、django-extensionsのshell_plusについて紹介します(会社で先輩から教えてもらいました)。

django-extensionsとは

公式ドキュメント

Django拡張機能群のライブラリです。

テンプレートタグの生成を手助けしてくれるcreate_template_tagsやデバッガが組み込まれたRunServerPlusといったものが色々と用意されてるようです。

その中の1つが、今回紹介するshell_plusです。

shell_plusとは

shell_plus

Django shell with autoloading of the apps database models and subclasses of user-defined classes.

自動でアプリデータベースモデルとサブクラス(ユーザー定義クラス)を自動ロードしてくれる、とのことです。

これだけだと最初イメージが湧きませんでしたが、実際に立ち上げてみると分かりやすかったので、まずは一度立ち上げてみることにします。

shell_plusIPython bpython ptpython などにも対応しています。

The default resolution order is: ptpython, bpython, ipython, python.

との記載があり、既にインストール済みのものがあると、上記の順に優先されます。

起動すると、以下のようになります。

(venv) tatsuya@MacBook-Pro mysite % python manage.py shell_plus
# Shell Plus Model Imports
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import Group, Permission, User
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session
from tasks.models import Task
# Shell Plus Django Imports
from django.core.cache import cache
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import transaction
from django.db.models import Avg, Case, Count, F, Max, Min, Prefetch, Q, Sum, When
from django.utils import timezone
from django.urls import reverse
from django.db.models import Exists, OuterRef, Subquery
Python 3.12.1 (v3.12.1:2305ca5144, Dec  7 2023, 17:23:38) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 

# Shell Plus Django Importsの1行上に、私の場合は自作のTaskモデルが存在するのでTaskモデルが読み込まれていることが分かります。

また、その他にもfrom django.contrib.auth.models import Group, Permission, Userといったモデル群や、from django.db.models import Avg, Case, Count, F, Max, Min, Prefetch, Q, Sum, Whenといったものが読み込まれているので、すぐにQuerySetを実行できる準備が整っていることがわかります。

あとは、実際にQuerySetを書いていくことで、実際に実行することができます。

>>> Task.objects.filter(id=1)
<QuerySet [<Task: Task object (1)>]>

--print-sqlについて

そして、調べていく中で分かったオプションとして、--print-sqlというオプションが存在しました。

これは、DEBUG=Trueの場合に有効になる機能で、その名前の通りSQLをprintしてくれます。

例えば、以下のようになります。

>>> Task.objects.filter(id=1)
SELECT "task"."id",
       "task"."content",
       "task"."created_user_id",
       "task"."updated_user_id",
       "task"."created_at",
       "task"."updated_at"
  FROM "task"
 WHERE "task"."id" = 1
 LIMIT 21
Execution time: 0.001414s [Database: default]
<QuerySet [<Task: Task object (1)>]>
>>> 

上記のようなコードでは「なんとなく」SELECT * FROM task WHERE id = 1 のようなSQLが実行されている、と思っていても実際には内部的には違う結果を返していることが分かりました。

他にも、ちょっと複雑なQuerySetを実行する時にも便利です。QuerySetを書くとき、また書いた後にもどのようなSQLが発行されているか、確認することは大切です。

参考(所属企業ビープラウドの自走プログラマーより): 60:Django ORMでどんなSQLが発行されているか気にしよう

taskを更新したユーザーの中で、最も更新したユーザーと更新回数を取得する・・・といった具合にQuerySetを書いてみたとして。

  • 画面を開いて確認する
  • 間違っていたら修正する
  • 修正後、最後画面を開いて確認する...

というプロセスを踏むより、効率的に調べることができます。

今回で言うと、一度書いたコードは以下のようになりました。

>>> Task.objects.values('updated_user').annotate(count=Count('id')).order_by('-count').first()
SELECT "task"."updated_user_id",
       COUNT("task"."id") AS "count"
  FROM "task"
 GROUP BY "task"."updated_user_id"
 ORDER BY 2 DESC
 LIMIT 1
Execution time: 0.000209s [Database: default]
{'updated_user': 1, 'count': 4}

これを踏まえても最初、ORDER BY 2 DESC の部分でcountじゃなく2なのかと驚きましたし、どのように裏側で実行されているかをチェックするのは大事だなと思いました。

このように活用することで、より快適に効率よく進められることがわかったので、私自身も活用していこうと思います。