ORM

Django ORMでSQL直接実行|raw()とconnection.cursorの使い方

DjangoのORMでは表現しきれない複雑なクエリや、パフォーマンス重視の処理には、SQLを直接実行する方法があります。raw()メソッドやconnection.cursor()を使ったSQL直接実行の方法を解説します。

基本的な使い方

views.py
persons = Person.objects.raw('SELECT * FROM myapp_person WHERE age > %s', [25])

for person in persons:
    print(person.name)

説明

Step 1Raw SQLの基本

DjangoのORMは多くの場合十分強力ですが、特殊なデータベース機能を使用したい場合や、非常に複雑なクエリが必要な場合は、直接SQLを実行することができます。Djangoでは、raw()メソッドを使用して生のSQL文を実行できます。

# 基本的な使い方
モデル名.objects.raw(SQL文, パラメータ)

raw()メソッドは、モデルインスタンスのイテレータを返します。

Step 2基本的な使用例

例えば、特定のIDを持つPersonモデルのレコードを取得する場合:

# Personテーブルから特定のIDのデータを取得
person = Person.objects.raw('SELECT * FROM myapp_person WHERE id = %s', [person_id])

# 結果は反復処理可能
for p in person:
    print(p.name)

# 通常は1件だけの場合、最初の要素を取得
first_person = list(person)[0]  # or person[0]

注意: raw()メソッドの結果は、リストではなくイテレータです。すべての結果が必要な場合はlist()で変換します。

Step 3SQLインジェクション対策

Raw SQLを使用する際は、SQLインジェクション攻撃に対する対策が重要です。パラメータは文字列内に直接埋め込まず、パラメータ化クエリを使用します:

# 安全な方法(推奨)
Person.objects.raw('SELECT * FROM myapp_person WHERE last_name = %s', [last_name])

# 危険な方法(避けるべき)
Person.objects.raw('SELECT * FROM myapp_person WHERE last_name = "' + last_name + '"')  # 危険!

パラメータはデータベースバックエンドによって適切にエスケープされます。

Step 4モデルフィールドのマッピング

raw()メソッドは、SQLの結果をモデルインスタンスにマッピングします。このとき、少なくともモデルの主キーを選択する必要があります:

# 必要なフィールドだけを選択(主キーは必須)
persons = Person.objects.raw('SELECT id, name, age FROM myapp_person')

# 別名を使用する場合
persons = Person.objects.raw('''
    SELECT 
        id, 
        first_name || ' ' || last_name AS full_name,
        EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM birth_date) AS calculated_age
    FROM myapp_person
''')

# 結果へのアクセス
for person in persons:
    print(f"{person.full_name} is {person.calculated_age} years old")

SQLで計算したフィールドや別名を付けたフィールドも、モデルインスタンスのプロパティとしてアクセスできます。

Step 5複雑なクエリの例

Raw SQLは、ORMでは表現しにくい複雑なクエリに特に有用です:

# 複雑なJOINとサブクエリ
products = Product.objects.raw('''
    SELECT 
        p.id, 
        p.name, 
        p.price,
        (SELECT AVG(price) FROM myapp_product) AS avg_price,
        p.price - (SELECT AVG(price) FROM myapp_product) AS price_diff,
        COUNT(s.id) AS sales_count
    FROM 
        myapp_product p
    LEFT JOIN 
        myapp_sale s ON p.id = s.product_id
    GROUP BY 
        p.id, p.name, p.price
    HAVING 
        COUNT(s.id) > 5
    ORDER BY 
        sales_count DESC
''')

# データベース固有の関数を使用
from django.db import connection

if connection.vendor == 'postgresql':
    # PostgreSQL固有の関数を使用
    persons = Person.objects.raw('''
        SELECT 
            id, 
            name, 
            age,
            EXTRACT(EPOCH FROM last_login) AS login_epoch
        FROM myapp_person
        WHERE 
            last_login > NOW() - INTERVAL '30 days'
    ''')
elif connection.vendor == 'mysql':
    # MySQL固有の構文
    persons = Person.objects.raw('''
        SELECT 
            id, 
            name, 
            age,
            UNIX_TIMESTAMP(last_login) AS login_epoch
        FROM myapp_person
        WHERE 
            last_login > DATE_SUB(NOW(), INTERVAL 30 DAY)
    ''')

Step 6実践的な使用例

views.pyでのRaw SQLの使用例:

from django.shortcuts import render
from django.db import connection
from .models import Product, Sale

def sales_report(request):
    # 標準のORMクエリでは複雑すぎる分析クエリ
    products = Product.objects.raw('''
        WITH monthly_sales AS (
            SELECT 
                product_id,
                DATE_TRUNC('month', date) AS month,
                SUM(amount) AS monthly_total
            FROM 
                myapp_sale
            GROUP BY 
                product_id, DATE_TRUNC('month', date)
        )
        SELECT 
            p.id,
            p.name,
            p.price,
            SUM(s.amount) AS total_sales,
            COUNT(DISTINCT s.customer_id) AS unique_customers,
            MAX(ms.monthly_total) AS best_month_sales
        FROM 
            myapp_product p
        LEFT JOIN 
            myapp_sale s ON p.id = s.product_id
        LEFT JOIN 
            monthly_sales ms ON p.id = ms.product_id
        GROUP BY 
            p.id, p.name, p.price
        ORDER BY 
            total_sales DESC
    ''')
    
    return render(request, 'products/sales_report.html', {
        'products': products
    })

def database_info(request):
    # データベースのメタデータ取得
    with connection.cursor() as cursor:
        if connection.vendor == 'postgresql':
            cursor.execute('''
                SELECT 
                    table_name, 
                    pg_size_pretty(pg_total_relation_size(quote_ident(table_name))) AS total_size
                FROM 
                    information_schema.tables
                WHERE 
                    table_schema = 'public'
                ORDER BY 
                    pg_total_relation_size(quote_ident(table_name)) DESC
            ''')
            tables = cursor.fetchall()
        elif connection.vendor == 'mysql':
            cursor.execute('''
                SELECT 
                    table_name,
                    ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_in_mb
                FROM 
                    information_schema.tables
                WHERE 
                    table_schema = DATABASE()
                ORDER BY 
                    (data_length + index_length) DESC
            ''')
            tables = cursor.fetchall()
    
    return render(request, 'admin/database_info.html', {
        'tables': tables
    })

テンプレートでの使用例(sales_report.html):

<h1>商品売上レポート</h1>

<table>
    <tr>
        <th>商品名</th>
        <th>価格</th>
        <th>総売上</th>
        <th>顧客数</th>
        <th>最高月間売上</th>
    </tr>
    {% for product in products %}
        <tr>
            <td>{{ product.name }}</td>
            <td>{{ product.price|floatformat:0 }}円</td>
            <td>{{ product.total_sales|floatformat:0 }}円</td>
            <td>{{ product.unique_customers }}人</td>
            <td>{{ product.best_month_sales|floatformat:0 }}円</td>
        </tr>
    {% endfor %}
</table>
重要ポイント:
  • raw()メソッドを使用する場合は、少なくともモデルの主キー(通常はid)をSELECTに含める必要があります。
  • SQLインジェクションを防ぐため、常にパラメータ化クエリを使用してください。
  • データベース固有の機能を使用する場合は、connection.vendorでデータベースタイプを確認し、適切なSQL構文を使用してください。
  • 複雑なデータベース操作が必要な場合は、connection.cursor()を使用して直接カーソルを取得することもできます。
  • Raw SQLはDjangoのORMと比較して可読性やメンテナンス性が低下する可能性があるため、必要な場合にのみ使用するのが良いでしょう。
  • データベースの移行時にRaw SQLは変更が必要になる可能性があることに注意してください。

まとめ

  • Model.objects.raw()でSQLを直接実行し、モデルインスタンスとして取得できる
  • connection.cursor()で任意のSQLを実行できる
  • SQLインジェクション対策として、必ずパラメータ化クエリを使用する
  • ORMで表現できない複雑なクエリやパフォーマンスが必要な場面で使用する
  • 保守性の観点から、可能な限りORMを使い、Raw SQLは最後の手段とする
  • データベース固有のSQL構文を使う場合はポータビリティに注意する