اجرای کوئری‌های SQL خام در Django: RawSQL، raw() و اجرای مستقیم SQL

این مقاله سه روش Django برای اجرای SQL خام را توضیح می‌دهد: استفاده از RawSQL در داخل QuerySet، استفاده از متد raw() برای بازگرداندن نمونه‌های مدل، و اجرای مستقیم SQL بدون لایهٔ مدل. همچنین نحوهٔ نگاشت فیلدها، پارامترگذاری امن، محدودیت‌ها، deferred fields، و هشدارهای امنیتی مربوط به SQL injection بررسی می‌شود.

raw SQL، RawSQL، raw()، SQL injectionparameterized queriesRawQuerySet، Django ORM

~5 دقیقه مطالعه • بروزرسانی ۱۹ اسفند ۱۴۰۴

۱. مقدمه

Django سه روش برای اجرای SQL خام ارائه می‌دهد:

  • استفاده از RawSQL در داخل QuerySet
  • استفاده از Manager.raw() برای بازگرداندن نمونه‌های مدل
  • اجرای مستقیم SQL بدون استفاده از مدل

قبل از استفاده از SQL خام، همیشه بررسی کنید که آیا ORM Django نیاز شما را پوشش می‌دهد یا نه. ORM ابزارهای بسیار قدرتمندی برای فیلتر، annotate، aggregate و ساخت queryهای پیچیده دارد.

۲. استفاده از RawSQL در QuerySet

گاهی لازم است یک قطعه SQL خام را در داخل annotate یا filter استفاده کنید. برای این کار از RawSQL یا Func استفاده می‌شود.

۳. اجرای کوئری خام با raw()

متد raw() روی Manager امکان اجرای SQL خام و دریافت نمونه‌های مدل را فراهم می‌کند.

مثال:


for p in Person.objects.raw("SELECT * FROM myapp_person"):
    print(p)

این روش یک RawQuerySet برمی‌گرداند که مانند QuerySet قابل پیمایش است.

نکته درباره نام جدول

Django نام جدول را از app label و نام مدل می‌سازد، مگر اینکه db_table را تغییر داده باشید.

هشدار:

  • Django هیچ بررسی روی SQL انجام نمی‌دهد.
  • اگر کوئری شما ردیفی برنگرداند، ممکن است خطاهای عجیب رخ دهد.
  • در MySQL، تبدیل خودکار نوع‌ها می‌تواند نتایج غیرمنتظره ایجاد کند.

۴. نگاشت فیلدهای SQL به فیلدهای مدل

raw() فیلدهای کوئری را بر اساس نام به فیلدهای مدل نگاشت می‌کند. ترتیب ستون‌ها مهم نیست.

مثال:


SELECT last_name AS last_name, first_name AS first_name, id AS id FROM ...

یا با استفاده از translations:


name_map = {"first": "first_name", "last": "last_name"}
Person.objects.raw("SELECT * FROM some_table", translations=name_map)

۵. ایندکس‌گذاری روی RawQuerySet

می‌توانید از ایندکس استفاده کنید:


first_person = Person.objects.raw("SELECT * FROM myapp_person")[0]

اما این ایندکس‌گذاری در سطح Python انجام می‌شود، نه SQL. برای کارایی بهتر:


SELECT * FROM myapp_person LIMIT 1

۶. حذف فیلدها (Deferred Fields)

می‌توانید برخی فیلدها را در SELECT حذف کنید:


Person.objects.raw("SELECT id, first_name FROM myapp_person")

فیلدهای حذف‌شده هنگام دسترسی lazy load می‌شوند.

نکته مهم: فیلد primary key باید همیشه در کوئری باشد.

۷. ارسال پارامترهای امن به raw()

برای جلوگیری از SQL injection، همیشه از params استفاده کنید:


lname = "Doe"
Person.objects.raw("SELECT * FROM myapp_person WHERE last_name = %s", [lname])

برای دیکشنری:


WHERE last_name = %(lname)s

SQLite از دیکشنری پشتیبانی نمی‌کند.

هشدار امنیتی:

هرگز از string formatting استفاده نکنید:


"WHERE last_name = %s" % lname  # خطرناک

و هرگز placeholder را quote نکنید:


"WHERE last_name = '%s'"  # اشتباه

این کار شما را در برابر SQL injection آسیب‌پذیر می‌کند.

جمع‌بندی

اجرای SQL خام در Django ابزار قدرتمندی است، اما باید با دقت و فقط زمانی استفاده شود که ORM پاسخگو نباشد. RawSQL برای قطعات کوچک SQL، raw() برای بازگرداندن نمونه‌های مدل، و اجرای مستقیم SQL برای کنترل کامل مناسب است. همیشه از پارامترگذاری امن استفاده کنید و از string formatting پرهیز کنید.

۱. مقدمه

گاهی اوقات Manager.raw() کافی نیست—مثلاً زمانی که کوئری شما به مدل‌ها نگاشت نمی‌شود یا نیاز دارید مستقیماً UPDATE، INSERT یا DELETE اجرا کنید. در این مواقع می‌توانید کاملاً از لایهٔ مدل عبور کرده و مستقیماً با دیتابیس کار کنید.

ورودی اصلی این کار django.db.connection است که اتصال پیش‌فرض دیتابیس را نشان می‌دهد.

۲. اجرای SQL با Cursor

برای اجرای SQL، ابتدا یک cursor بگیرید و سپس execute() را فراخوانی کنید:


from django.db import connection

def my_custom_sql(self):
    with connection.cursor() as cursor:
        cursor.execute("UPDATE bar SET foo = 1 WHERE baz = %s", [self.baz])
        cursor.execute("SELECT foo FROM bar WHERE baz = %s", [self.baz])
        row = cursor.fetchone()
    return row

نکتهٔ امنیتی: هرگز دور placeholderهای %s کوتیشن نگذارید. Django خودش مقدار را escape می‌کند.

استفاده از علامت درصد (%) در کوئری

اگر کوئری شما شامل % باشد، هنگام استفاده از پارامترها باید آن را escape کنید:


cursor.execute("SELECT foo FROM bar WHERE baz = '30%'")
cursor.execute("SELECT foo FROM bar WHERE baz = '30%%' AND id = %s", [self.id])

۳. کار با چند دیتابیس

اگر چند دیتابیس دارید، از django.db.connections استفاده کنید:


from django.db import connections

with connections["my_db_alias"].cursor() as cursor:
    ...

۴. بازگرداندن نتایج به‌صورت dict

به‌طور پیش‌فرض، cursor نتایج را به‌صورت tuple برمی‌گرداند. برای دریافت dict می‌توانید از این تابع استفاده کنید:


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

۵. بازگرداندن نتایج به‌صورت namedtuple

گزینهٔ دیگر استفاده از namedtuple است:


from collections import namedtuple

def namedtuplefetchall(cursor):
    desc = cursor.description
    nt_result = namedtuple("Result", [col[0] for col in desc])
    return [nt_result(*row) for row in cursor.fetchall()]

این روش امکان دسترسی به فیلدها با نام یا ایندکس را فراهم می‌کند.

مقایسهٔ سه روش:


cursor.execute("SELECT id, parent_id FROM test LIMIT 2")
cursor.fetchall()
# → ((54360982, None), (54360880, None))

dictfetchall(cursor)
# → [{'id': 54360982, 'parent_id': None}, ...]

namedtuplefetchall(cursor)
# → [Result(id=54360982, parent_id=None), ...]

۶. Connection و Cursor

cursor و connection تقریباً مطابق استاندارد Python DB-API (PEP 249) عمل می‌کنند، به‌جز مدیریت تراکنش‌ها.

همیشه از placeholderهای %s استفاده کنید—even برای SQLite.

استفاده از cursor به‌عنوان context manager


with connection.cursor() as c:
    c.execute(...)

این معادل است با:


c = connection.cursor()
try:
    c.execute(...)
finally:
    c.close()

۷. فراخوانی Stored Procedure

برای فراخوانی stored procedure از cursor.callproc() استفاده کنید:


with connection.cursor() as cursor:
    cursor.callproc("test_procedure", [1, "test"])

فقط Oracle از پارامترهای keyword پشتیبانی می‌کند.

جمع‌بندی

اجرای مستقیم SQL در Django زمانی مفید است که ORM پاسخگو نباشد. با استفاده از cursor می‌توانید کوئری‌های پیچیده، عملیات نوشتنی، کار با چند دیتابیس و فراخوانی stored procedureها را انجام دهید. همیشه از پارامترگذاری امن استفاده کنید و از string formatting برای ساخت کوئری‌ها پرهیز کنید تا از SQL injection جلوگیری شود.

نوشته و پژوهش شده توسط دکتر شاهین صیامی