はじめに
自サイトでDjangoを読むというソースコード読解をやっています。
その中で、モデル保存の部分を読んでいるときに「これってなんかのメソッド呼んだら実行されるSQL表示できないのかな」と思ったのですがなさそうなので「こうやれば表示できる」という方法を投稿しておきます。
チュートリアル確認
Djangoチュートリアル その2では次の手順でモデルをDBに保存しています。
>>> from polls.models import Question, Choice
>>> from django.utils import timezone
>>> question = Question(question_text="What's new?", pub_date=timezone.now())
>>> question.save()
が、ここでsaveメソッドを呼ぶとSQLを構築してDBへの保存まで一気にされてしまいます。というわけでsaveメソッドを呼ぶのではなく、saveメソッドの中で行われていることの重要部分だけを手動で再現して実行されるSQLを出力しました。
準備その1:保存先DBの決定
まずはsaveメソッドで行われている処理の抜粋。
>>> cls = question.__class__
>>> from django.db import router
>>> using = router.db_for_write(cls, instance=question)
モデルのクラス(クラスオブジェクト)はこの後も使うので変数に入れておきます。
django.db.routerというのはDBを分散している場合にどのDBに保存するかの決定する仕組みです。分散していなければ普通は'default'になります。
準備その2:フィールド情報とかの取得
saveメソッドから処理が進んでいくと_save_tableというメソッドに着きます。この中でSQLを構築するために必要なものだけ取り出すと、
>>> from django.db.models.fields import AutoField
>>> meta = cls._meta
>>> fields = meta.local_concrete_fields
>>> fields = [f for f in fields if not isinstance(f, AutoField)]
>>> manager = cls._base_manager
metaというのはdb.models.optionsモジュールのOptionsクラスで、この中にフィールド情報とかが全部入っています。からくりについてはご興味があれば私の読解記事を参照してください。
managerは大雑把に言うとモデルに関する処理を取り仕切っているクラスです。
初回(INSERT)なのか更新(UPDATE)なのかで分岐しますが今回はINSERTなので、do_insertで行われている処理をしておきます。
>>> objs = [question]
INSERT SQLの表示
というわけで準備が整ったので、以下を実行するとSQLが表示されます。なお、managerの属性のmodelはModelインスタンスではなくModelクラスです。
>>> from django.db.models import sql
>>> query = sql.InsertQuery(manager.model)
>>> query.insert_values(fields, objs)
>>> compiler = query.get_compiler(using=using)
>>> compiler.return_id = True
>>> compiler.as_sql()
[('INSERT INTO "polls_question" ("question_text", "pub_date") VALUES (%s, %s)', ["What's new?", '2017-07-24 13:35:55.856747'])]
これらのコードはModelクラスではなく、django.db.models.managerモジュールのManagerクラスに書かれています。というのは半分嘘で、Managerクラスは継承関係がめんどくさく、下記の処理(_insertメソッドの抜粋)はdjango.db.models.queryモジュールのQuerySetクラスにあります。
おまけ
上記の処理を一度に行う関数を作りました。
from django.db import router
from django.db.models.fields import AutoField
from django.db.models import sql
def show_insert_sql(instance):
cls = instance.__class__
using = router.db_for_write(cls, instance=instance)
meta = cls._meta
fields = meta.local_concrete_fields
fields = [f for f in fields if not isinstance(f, AutoField)]
manager = cls._base_manager
objs = [question]
query = sql.InsertQuery(manager.model)
query.insert_values(fields, objs)
compiler = query.get_compiler(using=using)
compiler.return_id = True
return compiler.as_sql()