Djangoを利用するとき、QuerySetを利用してSQLを組み立てるケースが多いと思います。
しかし、QuerySetの経験に乏しいと、SQLがイメージできていても、どういうQuerySetを書けば良いか、というのがまだ判断できないケースも存在すると思います。
動作確認実施時に、決められた手順で作業をするものの、そのために何度も手順を繰り返すのも効率が悪かったりします。
そういう時に便利な、django-extensionsのshell_plusについて紹介します(会社で先輩から教えてもらいました)。
django-extensionsとは
テンプレートタグの生成を手助けしてくれるcreate_template_tags
やデバッガが組み込まれたRunServerPlus
といったものが色々と用意されてるようです。
その中の1つが、今回紹介するshell_plus
です。
shell_plusとは
Django shell with autoloading of the apps database models and subclasses of user-defined classes.
自動でアプリデータベースモデルとサブクラス(ユーザー定義クラス)を自動ロードしてくれる、とのことです。
これだけだと最初イメージが湧きませんでしたが、実際に立ち上げてみると分かりやすかったので、まずは一度立ち上げてみることにします。
shell_plus
はIPython
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
なのかと驚きましたし、どのように裏側で実行されているかをチェックするのは大事だなと思いました。
このように活用することで、より快適に効率よく進められることがわかったので、私自身も活用していこうと思います。