django_migrationsテーブルについて

django_migrationsマイグレーション履歴を保持するテーブル。

しかし、意外とドキュメントに説明はない。 主にチュートリアル part 2 に以下の説明がある通りである。

Django tracks which ones are applied using a special table in your database called django_migrations

djangoリポジトリを検索し、チュートリアル part2を含めて記載箇所は以下の3箇所のようだった。

コードの配置場所

django/db/migrations/recorder.pydjango_migrationsのコードが存在している。

https://github.com/django/django/blob/main/django/db/migrations/recorder.py

If a migration is unapplied its row is removed from the table. Having a row in the table always means a migration is applied.

とあり、マイグレーションの適用状態をテーブルに記録していることがわかる。

MigrationRecorderクラスのMigrationメソッド内部にMigrationクラスがあり、そこでModelの定義がされている。

メソッド内部にクラスがある書き方は見慣れなかったが、docstringにはAppRegistryNotReady を防ぐための遅延ロードの手法であることが書かれていた。

Lazy load to avoid AppRegistryNotReady if installed apps import MigrationRecorder.

モデルの記録内容

記録する内容は以下の3つ。

具体例を見ると、以下のようになっていた。

postgres=# SELECT * FROM django_migrations WHERE app='tasks';
 id |  app  |                name                |            applied            
----+-------+------------------------------------+-------------------------------
 19 | tasks | 0001_initial                       | 2024-04-06 04:44:41.661527+00
 20 | tasks | 0002_task_title_alter_task_content | 2024-04-06 04:44:41.671349+00
 21 | tasks | 0003_task_limit_date               | 2024-04-06 04:44:41.677108+00
 22 | tasks | 0004_task_status                   | 2024-04-06 04:44:41.682666+00
 23 | tasks | 0005_task_order                    | 2024-04-06 04:44:41.687782+00
 24 | tasks | 0006_alter_task_status             | 2024-04-06 04:44:41.692116+00
(6 rows)

その他のメソッド

そのほか、以下が定義されていた。

  • migration_qs
  • has_table
  • ensure_schema
  • applied_migrations
  • record_applied
  • record_unapplied
  • flush

この中のrecord_appliedによって、マイグレーション適用時の記録が書き込こまれ、record_unappliedによってマイグレーションロールバックなど、適用されなくなったときには削除されているようだった。

雑談

今までと文体が変わったが、この書き方の方が調べ物をしたときはスムーズだったので、試しに公開してみた。 このまま継続する...かもしれないし、継続しないかもしれない。 自己学習の記録であることを考えると、このままなのかな、という気はしている。

psqlコマンドが使えなかったがPATHを追加する必要があったという話

PostgreSQLをターミナル上で動かす際にpsqlというものがあります。

このpsqlが使えなかったケースがあり、結論としてはPATHが通っていなかったという状態でした。

Mac Ventura 13.6とHomebrewは4.2.17 の環境で確認しました。

また、以下の参考記事と内容はほぼ同一です。プラスアルファでインストール先が分からなかったので調べた、という差分です。

※結果としては記事と同じ場所に配置されていました。

【Mac/PostgreSQL】psqlコマンドが使えないときの対処法

状態の確認

まずは、インストール済みのPostgreSQLのバージョンを確認します。

tatsuya@MacBook-Pro ~ % brew search postgresql
==> Formulae
postgresql@10       postgresql@12       postgresql@14 ✔     postgresql@16       postgrest
postgresql@11       postgresql@13       postgresql@15       qt-postgresql

brew searchコマンドを使うと、インストール済のpostgresqlが表示されます。

この段階で、psql --versionと入力すると実行できると思っていましたが、zsh: command not found: psqlとなり、実行できませんでした。

そこで冒頭の記事同様に手順を進めて PATH を書く必要があるので書こうとしましたが、インストール先がわからない状態でした。

解決策

【Mac】brew --prefix でパッケージのインストールパスを取得する という記事に従って、brew --prefix を実行します。

※この記事は先輩から教えてもらいました。

今回は postgresql@14のインストール先が知りたいので、入力すると、結果がわかりました。

tatsuya@MacBook-Pro ~ % brew --prefix postgresql@14
/usr/local/opt/postgresql@14

実行パスがわかったので、.zshrcexport PATH="/usr/local/opt/postgresql@14/bin:$PATH"を追加して、ターミナルを再起動するとpsqlが実行できるようになりました。

tatsuya@MacBook-Pro ~ % psql --version
psql (PostgreSQL) 14.9 (Homebrew)

マイグレーションの実行前後ではshowmigrationsを実行しよう

Djangoマイグレーションの実行状態を確認する方法として、showmigrationsがあります。

https://docs.djangoproject.com/ja/5.0/ref/django-admin/#showmigrations

コマンドは以下のように実行します。

python manage.py showmigrations

以下のように実行されているかどうか、(あるいはマイグレーションファイルが無いこと)が出力されます。

コードはこちらのリポジトリを基にしています(2024/04/06現在のコードに対して、適当にモデルを変更するよう書き加えました)。

https://github.com/nibuno/djangoTaskApp

accounts
 (no migrations)
admin
 [X] 0001_initial
 [X] 0002_logentry_remove_auto_add
 [X] 0003_logentry_add_action_flag_choices
auth
 [X] 0001_initial
 [X] 0002_alter_permission_name_max_length
 [X] 0003_alter_user_email_max_length
 [X] 0004_alter_user_username_opts
 [X] 0005_alter_user_last_login_null
 [X] 0006_require_contenttypes_0002
 [X] 0007_alter_validators_add_error_messages
 [X] 0008_alter_user_username_max_length
 [X] 0009_alter_user_last_name_max_length
 [X] 0010_alter_group_name_max_length
 [X] 0011_update_proxy_permissions
 [X] 0012_alter_user_first_name_max_length
contenttypes
 [X] 0001_initial
 [X] 0002_remove_content_type_name
sessions
 [X] 0001_initial
tasks
 [X] 0001_initial
 [X] 0002_task_title_alter_task_content
 [X] 0003_task_limit_date
 [X] 0004_task_status
 [X] 0005_task_order
 [X] 0006_alter_task_status
 [ ] 0007_alter_task_table

このコマンドを実行することで、マイグレーション適用の状態を確認することができます。

引数まわり

python manage.py showmigrations tasks のようにアプリケーションを指定することで、特定のアプリケーションのマイグレーション状態を確認できます。おそらく、指定して確認するケースは結構使うんじゃ無いかなと思います。

tasks
 [X] 0001_initial
 [X] 0002_task_title_alter_task_content
 [X] 0003_task_limit_date
 [X] 0004_task_status
 [X] 0005_task_order
 [X] 0006_alter_task_status
 [ ] 0007_alter_task_table

--list または -l と言う引数がありますが、これはデフォルトで設定されているので挙動としては変わりません。

--plan または -p と言う引数もあります。これは全体を通したマイグレーションの実行計画が確認できるようでした。

python manage.py showmigrations tasks --plan を実行すると、以下のようになります。

[X]  contenttypes.0001_initial
[X]  auth.0001_initial
[X]  tasks.0001_initial
[X]  tasks.0002_task_title_alter_task_content
[X]  tasks.0003_task_limit_date
[X]  tasks.0004_task_status
[X]  tasks.0005_task_order
[X]  tasks.0006_alter_task_status
[ ]  tasks.0007_alter_task_table

どうやら、以下のコードがマイグレーションの最初に呼び出されているようでした。

https://github.com/django/django/blob/main/django/contrib/contenttypes/migrations/0001_initial.py

また、私はリファレンスを読んで初めて存在に気が付きましたが、django-admin に共通する引数として --verbosity と言うものが存在しており、以下のように実行することで、showmigrations (--list) の場合ではマイグレーション時間を確認することができます。

-python manage.py showmigrations tasks --verbosity 2

tasks
 [X] 0001_initial (applied at 2024-02-10 23:32:44)
 [X] 0002_task_title_alter_task_content (applied at 2024-02-10 23:32:44)
 [X] 0003_task_limit_date (applied at 2024-02-10 23:32:44)
 [X] 0004_task_status (applied at 2024-02-10 23:32:44)
 [X] 0005_task_order (applied at 2024-03-07 22:37:52)
 [X] 0006_alter_task_status (applied at 2024-03-09 22:51:47)
 [ ] 0007_alter_task_table

--plan の場合だと、依存するマイグレーションについても知ることができます。

python manage.py showmigrations tasks --plan --verbosity 2

[X]  contenttypes.0001_initial
[X]  auth.0001_initial ... (contenttypes.0001_initial)
[X]  tasks.0001_initial ... (auth.0001_initial)
[X]  tasks.0002_task_title_alter_task_content ... (tasks.0001_initial)
[X]  tasks.0003_task_limit_date ... (tasks.0002_task_title_alter_task_content)
[X]  tasks.0004_task_status ... (tasks.0003_task_limit_date)
[X]  tasks.0005_task_order ... (tasks.0004_task_status)
[X]  tasks.0006_alter_task_status ... (tasks.0005_task_order)
[ ]  tasks.0007_alter_task_table ... (tasks.0006_alter_task_status)

マイグレーションを実行してみる

あとは実際に、マイグレーションを実行して結果を確認します。

マイグレーションコマンド

python manage.py migrate tasks 0007_alter_task_table

showmigrationsコマンド

python manage.py showmigrations tasks --verbosity 2

出力結果

tasks
 [X] 0001_initial (applied at 2024-02-10 23:32:44)
 [X] 0002_task_title_alter_task_content (applied at 2024-02-10 23:32:44)
 [X] 0003_task_limit_date (applied at 2024-02-10 23:32:44)
 [X] 0004_task_status (applied at 2024-02-10 23:32:44)
 [X] 0005_task_order (applied at 2024-03-07 22:37:52)
 [X] 0006_alter_task_status (applied at 2024-03-09 22:51:47)
 [X] 0007_alter_task_table (applied at 2024-04-06 00:27:38)

[] 0007_alter_task_table が実行済みだと言うことがわかります。

また、ロールバックしてみます。

マイグレーションコマンド

python manage.py migrate tasks 0006_alter_task_status

showmigrationsコマンド

python manage.py showmigrations tasks --verbosity 2

出力結果

tasks
 [X] 0001_initial (applied at 2024-02-10 23:32:44)
 [X] 0002_task_title_alter_task_content (applied at 2024-02-10 23:32:44)
 [X] 0003_task_limit_date (applied at 2024-02-10 23:32:44)
 [X] 0004_task_status (applied at 2024-02-10 23:32:44)
 [X] 0005_task_order (applied at 2024-03-07 22:37:52)
 [X] 0006_alter_task_status (applied at 2024-03-09 22:51:47)
 [ ] 0007_alter_task_table

このように、showmigrations を活用することで、マイグレーションが実行されたかどうか、を確認することができます。

自分がマイグレーション実行の有無を確認したときにセットで書いた方が良いよ、と教えてもらったことがきっかけでした。

と言うのは私はマイグレーション実行後にしか利用していなかったので、正確さ・わかりやすさといった観点から教えてもらったためでした。

自分自身、こうした方が良いな〜 と思ったのでこれからは、マイグレーションの実行前後でshowmigrationsを実行していきます。

マイグレーションで出力されるSQLを確認する方法

DjangoでDBに関連した操作をするとき、django-adminmakemigrationsマイグレーションファイルを作成して、migrateマイグレーションを実行します。

そのマイグレーション実行時に実行されるSQLを確認するには、sqlmigrateを利用します。

Django4.2で検証しました。

sqlmigrate

https://docs.djangoproject.com/ja/4.2/ref/django-admin/#sqlmigrate

コマンドは以下のようになっています。

django-admin sqlmigrate app_label migration_name

app_labelはアプリケーション名、 migration_nameマイグレーションファイルの名前です。

例えば、taskテーブルをrenameする次のようなマイグレーションファイル0008_alter_task_table.pyが生成されたとします。

# Generated by Django 4.2.9 on 2024-03-28 11:25

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ("tasks", "0007_taskbackup"),
    ]

    operations = [
        migrations.AlterModelTable(
            name="task",
            table="task_rename",
        ),
    ]

その時、コマンドは以下のようになります。

django-admin sqlmigrate tasks 0008_alter_task_table

実行結果(SQLite3の場合)は次のような結果が返されます。

BEGIN;
--
-- Rename table for task to task_rename
--
ALTER TABLE "task" RENAME TO "task_rename";
COMMIT;

マイグレーションファイルについて

今回の例のマイグレーションファイル 0008_alter_task_table.py で内部で AlterModelTable が利用されています。

DBスキーマ操作用のクラスが定義されており、リファレンスで言うと以下のようなページがあります。

https://docs.djangoproject.com/ja/4.2/ref/migration-operations/#altermodeltable

定義しているモデルのテーブル名を変更します(Meta サブクラスの db_table オプションを参照します)。

と記述されており、大まかなレベルではリファレンスからどういうことをするか確認できます。

詳細に確認するには、sqlmigrateを実行したら良いんだな、という理解をしています。

SQLの定義場所

そして、SQLは以下に定義されています。

https://github.com/django/django/blob/main/django/db/backends/base/schema.py#L75

ここではこの変数をベースにSQLが発行されていたことが確認できます。

sql_rename_table = "ALTER TABLE %(old_table)s RENAME TO %(new_table)s"

また、今回はSQLite3の場合でしたが、MySQL PostgreSQLで実行できないSQLもあるので、それを吸収できるよう、それぞれのDBに合わせたディレクトリとファイル(及び変数)が存在します。

DBをMySQLに置き換えると、以下のようになります。

--
-- Rename table for task to task_rename
--
RENAME TABLE `task` TO `task_rename`;

コードは以下です。

https://github.com/django/django/blob/main/django/db/backends/mysql/schema.py#L7

sql_rename_table = "RENAME TABLE %(old_table)s TO %(new_table)s"

余談

何回か取り上げたSQLを確認しよう、というシリーズの延長線上になりましたが、Django任せにせず、どういう風にDBを操作しているかを確認するのも大事だなと思いました。

Djangoマイグレーションファイルも「Djangoがよしなに生成してくれるもの」くらいの認識でいましたが、マイグレーション用のクラスがあったことや、その裏側でSQLも定義されていることを改めて認識できて、世界が広がって面白いなぁと思いました。

django-admin startproject ではディレクトリを指定できる

きっかけはDocker, docker-composeを利用してコードを書いてみようと思ったときに、公式サンプルのコードを見ての疑問でした。

sudo docker compose run web django-admin startproject composeexample .

https://github.com/docker/awesome-compose/tree/master/official-documentation-samples/django/ より

Djangoチュートリアルでは以下のように記述されているのでそのまま使っていたので、 . ってなんだったかな? という感じでした。

$ django-admin startproject mysite

https://docs.djangoproject.com/en/5.0/intro/tutorial01/#creating-a-project

結論、. についてはLinuxコマンド上での、カレントディレクトリのことで、冒頭のコードとしては現在のディレクトリにprojectを作成する、ということでした。

コードを見てみる

startproject で実行されるのは django/core/management/commands/startproject.py です。

startprojectのコード自体は結構シンプルです。

from django.core.checks.security.base import SECRET_KEY_INSECURE_PREFIX
from django.core.management.templates import TemplateCommand

from ..utils import get_random_secret_key


class Command(TemplateCommand):
    help = (
        "Creates a Django project directory structure for the given project "
        "name in the current directory or optionally in the given directory."
    )
    missing_args_message = "You must provide a project name."

    def handle(self, **options):
        project_name = options.pop("name")
        target = options.pop("directory")

        # Create a random SECRET_KEY to put it in the main settings.
        options["secret_key"] = SECRET_KEY_INSECURE_PREFIX + get_random_secret_key()

        super().handle("project", project_name, target, **options)

https://github.com/django/django/blob/main/django/core/management/commands/startproject.py

ここでは

  • from django.core.management.templates から TemplateCommand を継承して Command を作成していること
  • name, directoryoptions から取得して基底クラスのhandle を呼び出し・渡している

ことが分かります。

基底クラスのhandle を呼び出し・渡した後

TemplateCommandhandleの冒頭部分は以下のようになっています。

def handle(self, app_or_project, name, target=None, **options):
        self.app_or_project = app_or_project
        self.a_or_an = "an" if app_or_project == "app" else "a"
        self.paths_to_remove = []
        self.verbosity = options["verbosity"]

        self.validate_name(name)

        # if some directory is given, make sure it's nicely expanded
        if target is None:
            top_dir = os.path.join(os.getcwd(), name)
            try:
                os.makedirs(top_dir)
            except FileExistsError:
                raise CommandError("'%s' already exists" % top_dir)
            except OSError as e:
                raise CommandError(e)
        else:
            top_dir = os.path.abspath(os.path.expanduser(target))
            if app_or_project == "app":
                self.validate_name(os.path.basename(top_dir), "directory")
            if not os.path.exists(top_dir):
                raise CommandError(
                    "Destination directory '%s' does not "
                    "exist, please create it first." % top_dir
                )

この else 以降の処理がディレクトリ名を引数として渡したときに実行される箇所ですが、os.path.abspath(os.path.expanduser(target)) という内容によって実際にディレクトリ名を指定・取得しているようでした。

https://github.com/django/django/blob/main/django/core/management/templates.py#L103-L111

とはいえパッと見て理解できなかったので、それぞれ分割して動かしてみると、以下のような挙動でした。

.のケース

>>> os.path.expanduser('.')
'.'
>>> os.path.abspath('.')
'/Users/tatsuya/django'

プロジェクト名のケース

>>> os.path.expanduser('djangoproject')
'djangoproject'
>>> os.path.abspath('djangoproject')
'/Users/tatsuya/django/djangoproject'

動かすだけではなくリファレンスについても見ておきましょう。

expanduserについてはこちら。

Unix および Windows では、与えられた引数の先頭のパス要素 ~ 、または ~user を、 user のホームディレクトリのパスに置き換えて返します。

https://docs.python.org/ja/3.12/library/os.path.html#os.path.expanduser

abspathについてはこちら。

パス名 path の正規化された絶対パスを返します。

https://docs.python.org/ja/3/library/os.path.html#os.path.abspath

自分の中では完全理解出来たかな、と言う状態です。

expanduserはどちらかというと ~~userを渡した時の挙動に関係するため、使わなくても良いのでは?と個人的に思いましたが、そこまで調べるのは時間がかかりそうなので一旦「こういう感じなんだな〜」と留めることにします。

あとは個人的に、目的別にCommandが存在していることが分かって面白かったので、 Command のコード周りを読むのも面白そうだなと感じました。

Djangoのログ出力について

先週投稿したOneToOneFieldの裏側で発行されているSQLを確認する の中でSQLをログ出力できるようにしていました。

そこについて、コピペで動いた!状態だったので、脱却するために調べました。

結論

DjangoではPythonloggingを利用しています。

logging.config.dictConfigをベースにした設定値を記述しているようでした。

コピペで動いていた設定値について

まず、先週は割愛していたLOGGING の内容を転記します。

LOGGING = {
    'version': 1,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'level': 'DEBUG',
            'propagate': False
        },
    },
}

60:Django ORMでどんなSQLが発行されているか気にしよう より引用しています。

定数 LOGGING

そもそもの定数値について。 リファレンスのページはこちら。

https://docs.djangoproject.com/en/5.0/ref/settings/#logging

前提として、Djangoにはデフォルトのログ設定がされています。

何も設定していない場合、例えばrunserver実行時にログが出ると思いますが、以下のリンク先の標準設定によってログ出力の内容が定義されています。

https://github.com/django/django/blob/main/django/utils/log.py#L18-L64

定数LOGGINGを設定することで、このログ設定を上書きするようになっている、という状態でした。

ref

詳細の内容については以下リンクに書かれています。

https://docs.djangoproject.com/en/5.0/ref/logging/

例えば、loggers では django.db.backends の設定がされていましたが、この内容についても書かれていて、SQLがDEBUGレベルで取得できることが分かります。

Messages relating to the interaction of code with the database. For example, every application-level SQL statement executed by a request is logged at the DEBUG level to this logger.

https://docs.djangoproject.com/en/5.0/ref/logging/#loggers

topics

また、概要について記述された以下のページが存在します。

https://docs.djangoproject.com/en/5.0/topics/logging/

ここでは、以下の4つについて触れていて、全体像がわかるように解説されています。

  • Logger
  • Handlers
  • Filters
  • Formatters

個人的にも最初、「それっぽい設定がされてあるのは分かるんだけど・・・」と思ったので、Djangoのloggingについて大まかに知るためには、まずここを読んだら良さそうでした(概要ですしね)。

それ以外にはversion という値についても記述がありました。

Identifies the configuration as being in ‘dictConfig version 1’ format. At present, this is the only dictConfig format version.

dictConfigPythonlogging.config.dictConfigのことで、以下のdictConfigのリファレンスにも書かれている内容がDjangoリファレンスでも書かれていることが確認できました。

version - to be set to an integer value representing the schema version. The only valid value at present is 1, but having this key allows the schema to evolve while still preserving backwards compatibility.

https://docs.python.org/3/library/logging.config.html#dictionary-schema-details

個人的には2, 3なども設定出来るかなと思ってましたが、これは設定できません。 ちなみに、設定した場合 ValueError が発生します。

Watching for file changes with StatReloader
Exception in thread django-main-thread:
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/threading.py", line 1073, in _bootstrap_inner
    self.run()
  File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/threading.py", line 1010, in run
    self._target(*self._args, **self._kwargs)
  File "/Users/tatsuya/djangoTaskApp/venv/lib/python3.12/site-packages/django/utils/autoreload.py", line 64, in wrapper
    fn(*args, **kwargs)
  File "/Users/tatsuya/djangoTaskApp/venv/lib/python3.12/site-packages/django/core/management/commands/runserver.py", line 125, in inner_run
    autoreload.raise_last_exception()
  File "/Users/tatsuya/djangoTaskApp/venv/lib/python3.12/site-packages/django/utils/autoreload.py", line 87, in raise_last_exception
    raise _exception[1]
  File "/Users/tatsuya/djangoTaskApp/venv/lib/python3.12/site-packages/django/core/management/__init__.py", line 394, in execute
    autoreload.check_errors(django.setup)()
  File "/Users/tatsuya/djangoTaskApp/venv/lib/python3.12/site-packages/django/utils/autoreload.py", line 64, in wrapper
    fn(*args, **kwargs)
  File "/Users/tatsuya/djangoTaskApp/venv/lib/python3.12/site-packages/django/__init__.py", line 19, in setup
    configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
  File "/Users/tatsuya/djangoTaskApp/venv/lib/python3.12/site-packages/django/utils/log.py", line 76, in configure_logging
    logging_config_func(logging_settings)
  File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/logging/config.py", line 912, in dictConfig
    dictConfigClass(config).configure()
  File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/logging/config.py", line 516, in configure
    raise ValueError("Unsupported version: %s" % config['version'])
ValueError: Unsupported version: 2

howto

ref, topics以外にhowtoページも存在します。

https://docs.djangoproject.com/en/5.0/howto/logging/

色々な設定方法についての解説がありますが、ここでは他のページで見られなかった propagate について解説されています。

これは定義したロガーの親に伝播させるかどうかの設定が出来るようなので、特定の子ロガーだけのログ処理を実行させるようなケースに設定出来るようでした。

内部ではPythonのloggingを利用している

dictConfigのくだりでlogging.config.dictConfigだと、少し触れましたが、loggingはPythonの標準組み込みライブラリのlogging が内部的に利用されています。

そのため、class で設定する logging.StreamHandler などはDjango側で定義されたクラスもありますが、ベースは logging.handlersになるようでした。

e.g. https://docs.python.org/ja/3/library/logging.handlers.html#logging.StreamHandler

調べてみて

内部で何を利用しているか?というところの理解が大事だなーと思いました。調べていく上で、謎の設定値が少しずつ読み解けました。

logging もそこまで詳しく無いので調べたい気持ちもありますが、全てを調べるには時間が足りないので一旦このくらいで。

他にも以下ページにはログ出力する方法とかも書かれていて、ケースに応じて利用できると良さそうだなと思いました。

https://docs.djangoproject.com/ja/5.0/topics/logging/#id5

OneToOneFieldの裏側で発行されているSQLを確認する

この記事は(第157回)Python mini Hack-a-thon(ハイブリッド) の記事です。

先週、こちらの記事を投稿したところ、先輩からアドバイスをもらいました。

確かにSQLを出力せずに調べていた(その前段階で「?」だった)事もあったので、今回は出力してみることにします。

SQLの出力方法

いつもながら自走プログラマーの内容を参考に、settings.pyに記述します。設定内容はリンク先に記述されているので割愛します。

60:Django ORMでどんなSQLが発行されているか気にしよう

動かしてみる

まずはUserを取得してみます。

まず、結果は通常のauth.Userを利用しているので予想通り、auth_userを普通に取得した、と言う内容でした。

>>> nibu = User.objects.get(username="nibu")
(0.001) SELECT "auth_user"."id", 
              "auth_user"."password", 
              "auth_user"."last_login", 
              "auth_user"."is_superuser", 
              "auth_user"."username", 
              "auth_user"."first_name", 
              "auth_user"."last_name", 
              "auth_user"."email", 
              "auth_user"."is_staff", 
              "auth_user"."is_active", 
              "auth_user"."date_joined" 
       FROM "auth_user" 
       WHERE "auth_user"."username" = 'nibu' 
       LIMIT 21; 
args=('nibu',); alias=default

次に、more についても調べてみます。 こちらも予想通りでした。

※前回の記事の動画のコードを元にしたので、drivers_moreになっています。

>>> nibu_more = More.objects.get(user=nibu)
(0.000) SELECT "drivers_more"."id", 
              "drivers_more"."user_id", 
              "drivers_more"."address", 
              "drivers_more"."postal_code" 
       FROM "drivers_more" 
       WHERE "drivers_more"."user_id" = 1 
       LIMIT 21; 
args=(1,); alias=default

この次、nibu.moreだと自分の想定とはちょっと異なりました。 JOINではなく、drivers_moreを呼び出した状態になっています。

自分のイメージは結合しているのかな?と思っていましたが、SQL的には結合せず効率の良いものが発行されていたことがわかります。

>>> nibu.more
(0.000) SELECT "drivers_more"."id", 
              "drivers_more"."user_id", 
              "drivers_more"."address", 
              "drivers_more"."postal_code" 
       FROM "drivers_more" 
       WHERE "drivers_more"."user_id" = 1 
       LIMIT 21; 
args=(1,); alias=default
<More: More object (1)>

となると、nibu_moreからuserを参照した場合も同様かな?と思ったところ、同様でした。

>>> nibu_more.user
(0.002) SELECT "auth_user"."id", 
              "auth_user"."password", 
              "auth_user"."last_login", 
              "auth_user"."is_superuser", 
              "auth_user"."username", 
              "auth_user"."first_name", 
              "auth_user"."last_name", 
              "auth_user"."email", 
              "auth_user"."is_staff", 
              "auth_user"."is_active", 
              "auth_user"."date_joined" 
       FROM "auth_user" 
       WHERE "auth_user"."id" = 1 
       LIMIT 21; 
args=(1,); alias=default
<User: nibu>

というわけで、どんなSQLが発行されているかを確認するのは大事だなと再認識しました。

もうちょっと複雑だったり、初めてみるQuerySet APIを利用したものはSQLを確認しようとしていましたが、こういう所から1つずつ意識していこうと思います。