◀ 4.

【Django】素のSQLを直接実行する方法(モデル使用/未使用)

▶
この記事の要点
  • モデルを使う: User.objects.raw('SELECT ... WHERE id = %s', [id]) でモデルのインスタンスとして取得
  • モデルを使わない: connection.cursor() → cursor.execute() → fetchone() / fetchall() でタプルとして取得
  • 値は必ず第 2 引数のパラメータで渡す。f-string や % で SQL に埋め込むと SQL インジェクションになる
  • プレースホルダは DB の種類に関係なく %s。クォートで囲まない
  • raw() の SELECT には主キー列を必ず含める

どちらを使うべきか

方法戻り値向いている用途
Model.objects.raw()モデルのインスタンス(RawQuerySet)複雑な SELECT だが、結果はモデルとして扱いたい
connection.cursor()タプル(行)集計結果、モデルに対応しない列、INSERT / UPDATE / DELETE、DB 固有の命令

まずは ORM(filter、annotate、Subquery など)で書けないか検討し、書けない・遅い場合に生の SQL を使うのが一般的な方針です。

モデル使用:Manager.raw()

from myapp.models import User

id = 1
users = User.objects.raw('SELECT id, name FROM user WHERE id = %s', [id])

for u in users:
    print(u.id, u.name)

raw() はクエリを実行して結果をモデルのインスタンスにマッピングします。1 件だけ取りたい場合は users[0] のようにインデックスで取れますが、0 件だと IndexError になります。

raw() の注意点

  • 主キー列を必ず SELECT する: 含めないと「Raw query must include the primary key」というエラーになる
  • テーブル名: Django が作るテーブル名は既定で「アプリ名_モデル名の小文字」(例: myapp_user)。Meta.db_table を指定していない場合は User._meta.db_table で確認して書く
  • SELECT しなかったフィールドは遅延読み込みになり、アクセスした時点で追加のクエリが発行される
  • モデルにない列(COUNT(*) AS cnt など)も、別名を付ければ u.cnt のように属性として読める
  • raw() の結果に filter() などの ORM メソッドは続けて使えない

モデル未使用:connection.cursor()

from django.db import connection

id = 1
with connection.cursor() as cursor:
    cursor.execute('SELECT id, name FROM user WHERE id = %s', [id])
    row = cursor.fetchone()      # (1, 'Taro') または None

print(row)

複数レコードを取得する場合は fetchone ではなく fetchall を使用します。

from django.db import connection

with connection.cursor() as cursor:
    cursor.execute('SELECT id, name FROM user WHERE age >= %s', [20])
    rows = cursor.fetchall()     # [(1, 'Taro'), (2, 'Hanako'), ...]

結果を辞書で受け取る

カーソルの結果はタプルなので、列名でアクセスしたい場合は cursor.description から列名を取り出して辞書に変換します。

def dictfetchall(cursor):
    columns = [col[0] for col in cursor.description]
    return [dict(zip(columns, row)) for row in cursor.fetchall()]

with connection.cursor() as cursor:
    cursor.execute('SELECT id, name FROM user')
    users = dictfetchall(cursor)   # [{'id': 1, 'name': 'Taro'}, ...]

INSERT / UPDATE / DELETE

from django.db import connection, transaction

with transaction.atomic():
    with connection.cursor() as cursor:
        cursor.execute('UPDATE user SET name = %s WHERE id = %s', ['Jiro', 1])
        print(cursor.rowcount)   # 更新された行数

Django は既定で自動コミット(autocommit)なので、execute した時点で確定します。複数の更新をまとめて成功・失敗させたい場合は transaction.atomic() で囲みます。

複数データベースを使っている場合

from django.db import connections

with connections['analytics'].cursor() as cursor:
    cursor.execute('SELECT COUNT(*) FROM access_log')
    total = cursor.fetchone()[0]

# raw() の場合
User.objects.using('analytics').raw('SELECT id, name FROM user')

パラメータの渡し方(SQL インジェクション対策)

値は必ず第 2 引数のリストで渡し、Django(DB ドライバ)にエスケープを任せます。

name = request.GET.get('name')

# OK: パラメータで渡す
cursor.execute('SELECT id FROM user WHERE name = %s', [name])

# NG: 文字列に埋め込む(SQL インジェクションの危険)
cursor.execute(f"SELECT id FROM user WHERE name = '{name}'")
cursor.execute("SELECT id FROM user WHERE name = '%s'" % name)

# NG: %s をクォートで囲む(値が二重にクォートされる)
cursor.execute("SELECT id FROM user WHERE name = '%s'", [name])
  • プレースホルダは SQLite・PostgreSQL・MySQL いずれでも %s(SQLite の ? は使わない)
  • 名前付きの %(name)s と辞書による指定は、DB バックエンドやバージョンによって対応が異なるため、リストと %s の組み合わせが最も無難
  • パラメータを渡すとき、SQL 内のリテラルの % は %% と書く必要がある。LIKE 検索は '%' + keyword + '%' をパラメータ側で組み立てるのが簡単
  • テーブル名・列名はパラメータにできない。動的に変える場合は許可リストで検証してから組み立てる
keyword = 'ta'
cursor.execute('SELECT id, name FROM user WHERE name LIKE %s', ['%' + keyword + '%'])

確認方法

  • 実際に発行された SQL は、DEBUG = True のとき connection.queries で確認できる
  • python manage.py dbshell で DB に直接接続し、同じ SQL を手で実行して結果を比較する
  • raw() の場合は print(users.query) でパラメータを埋めた SQL を表示できる
from django.db import connection
print(connection.queries[-1])   # {'sql': 'SELECT ...', 'time': '0.001'}

関連

Post Share
子ページ

子ページはありません

同階層のページ
  1. MySQL/MariaDBへの接続
  2. sqliteへの接続
  3. SELECT, INSERT, UPDATE, DELETE
  4. 素のSQLを直接実行する方法
  5. Order by DESCの指定方法
  6. limit, offsetの指定方法
  7. filterの検索オプション
  8. django-filterのlookup_expr検索オプション
  9. モデルの内部結合(1対1)