Spec-Zone.ru › Django 2.1

Выполнение произвольных SQL-запросов

Django предоставляет два способа выполнения произвольных SQL-запросов: вы можете использовать Manager.raw() для выполнения произвольных запросов и возврата экземпляров моделей, или же полностью обойти слой моделей и непосредственно выполнить пользовательский SQL.

Изучите ORM перед использованием сырого SQL!

Django ORM предоставляет множество инструментов для выражения запросов без написания сырого SQL. Например:

  • API QuerySet очень обширен.
  • Вы можете annotate и агрегировать с помощью многих встроенных базовых функций. Помимо этого, вы можете создавать собственные выражения запросов.

Перед использованием сырого SQL изучите ORM. Задайте вопрос на django-users или в #django IRC-канале, чтобы узнать, поддерживает ли ORM ваш случай использования.

Предупреждение

Следует проявлять особую осторожность при написании произвольного SQL. Каждый раз при использовании произвольного SQL нужно корректно экранировать любые параметры, которые может контролировать пользователь, используя params, чтобы защититься от атак SQL-инъекции. Подробнее об этом читайте в разделе защиты от SQL-инъекции.

Выполнение произвольных запросов

Метод менеджера raw() может быть использован для выполнения произвольных SQL-запросов, возвращающих экземпляры моделей:

Manager.raw(raw_query, params=None, translations=None)

Этот метод принимает произвольный SQL-запрос, выполняет его и возвращает экземпляр django.db.models.query.RawQuerySet. Этот экземпляр RawQuerySet может быть перебираем, как обычный QuerySet, для предоставления экземпляров объектов.

Это лучше всего продемонстрировать на примере. Предположим, у вас есть следующая модель:

class Person(models.Model):
    first_name = models.CharField(...)
    last_name = models.CharField(...)
    birth_date = models.DateField(...)

Вы можете затем выполнить произвольный SQL-запрос следующим образом:

>>> for p in Person.objects.raw('SELECT * FROM myapp_person'):
...     print(p)
John Smith
Jane Jones

Конечно, этот пример не очень интересен — он точно такой же, как и выполнение Person.objects.all(). Однако, raw() имеет много других возможностей, которые делают его очень мощным.

Имена таблиц моделей

Откуда в этом примере взялось имя таблицы Person?

По умолчанию Django определяет имя таблицы базы данных, соединяя "метку приложения" модели — имя, которое вы использовали в manage.py startapp — с именем класса модели, с подчёркиванием между ними. В примере мы предположили, что модель Person находится в приложении под названием myapp, так что её таблица будет myapp_person.

Для получения более подробной информации ознакомьтесь с документацией по параметру db_table, который также позволяет вручную задать имя таблицы базы данных.

Предупреждение

Никакой проверки не выполняется над SQL-запросом, переданным в .raw(). Django ожидает, что запрос вернёт набор строк из базы данных, но не делает ничего, чтобы это обеспечить. Если запрос не возвращает строк, результат будет (возможно, туманной) ошибкой.

Предупреждение

Если вы выполняете запросы на MySQL, имейте в виду, что неявное приведение типов в MySQL может привести к неожиданным результатам при смешивании типов. Если вы запрашиваете столбец строкового типа, но с целочисленным значением, MySQL приведёт тип всех значений в таблице к целочисленному перед выполнением сравнения. Например, если ваша таблица содержит значения 'abc', 'def', и вы запрашиваете WHERE mycolumn=0, обе строки будут соответствовать. Чтобы этого избежать, выполните правильное приведение типа перед использованием значения в запросе.

Сопоставление полей запроса с полями модели

raw() автоматически сопоставляет поля запроса с полями модели.

Порядок полей в вашем запросе не имеет значения. Другими словами, оба следующих запроса работают идентично:

>>> Person.objects.raw('SELECT id, first_name, last_name, birth_date FROM myapp_person')
...
>>> Person.objects.raw('SELECT last_name, birth_date, first_name, id FROM myapp_person')
...

Сопоставление выполняется по имени. Это означает, что вы можете использовать AS-суффиксы SQL для сопоставления полей запроса с полями модели. Таким образом, если у вас есть ещё одна таблица с данными Person, вы можете легко отобразить их в экземпляры Person:

>>> Person.objects.raw('''SELECT first AS first_name,
...                              last AS last_name,
...                              bd AS birth_date,
...                              pk AS id,
...                       FROM some_other_table''')

Достаточно совпадения имён, и экземпляры моделей будут созданы корректно.

В качестве альтернативы, вы можете сопоставить поля запроса с полями модели, используя аргумент translations к raw(). Это словарь, сопоставляющий имена полей в запросе с именами полей в модели. Например, вышеупомянутый запрос также можно записать:

>>> name_map = {'first': 'first_name', 'last': 'last_name', 'bd': 'birth_date', 'pk': 'id'}
>>> Person.objects.raw('SELECT * FROM some_other_table', translations=name_map)

Поиск по индексу

raw() поддерживает индексирование, поэтому, если вам нужен только первый результат, вы можете написать:

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

Однако, индексирование и срезы не выполняются на уровне базы данных. Если у вас большое количество Person объектов в вашей базе данных, эффективнее ограничить запрос на уровне SQL:

>>> first_person = Person.objects.raw('SELECT * FROM myapp_person LIMIT 1')[0]

Отсрочка загрузки полей модели

Поля также могут быть исключены:

>>> people = Person.objects.raw('SELECT id, first_name FROM myapp_person')

Person объекты, возвращаемые этим запросом, будут отложенными экземплярами модели (см. defer()). Это означает, что поля, исключённые из запроса, будут загружены по требованию. Например:

>>> for p in Person.objects.raw('SELECT id, first_name FROM myapp_person'):
...     print(p.first_name, # This will be retrieved by the original query
...           p.last_name) # This will be retrieved on demand
...
John Smith
Jane Jones

На первый взгляд, кажется, что запрос получил как имя, так и фамилию. Однако, на самом деле этот пример выполнил 3 запроса. Только имена были получены запросом raw() — фамилии были получены по требованию, когда они были выведены.

Есть только одно поле, которое нельзя исключить — поле первичного ключа. Django использует первичный ключ для идентификации экземпляров моделей, поэтому он всегда должен быть включён в сырой запрос. Если вы забудете включить первичный ключ, будет выброшено исключение InvalidQuery.

Добавление аннотаций

Вы также можете выполнять запросы, содержащие поля, которые не определены в модели. Например, мы можем использовать функцию age() PostgreSQL для получения списка людей с их возрастом, вычисленным в базе данных:

>>> people = Person.objects.raw('SELECT *, age(birth_date) AS age FROM myapp_person')
>>> for p in people:
...     print("%s is %s." % (p.first_name, p.age))
John is 37.
Jane is 42.
...

Вы часто можете избежать использования сырого SQL для вычисления аннотаций, используя вместо этого выражение Func().

Передача параметров в raw()

Если вам нужно выполнить параметризованные запросы, вы можете использовать аргумент params к raw():

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

params — это список или словарь параметров. Вы будете использовать %s заполнитель в строке запроса для списка или %(key)s заполнитель для словаря (где key заменяется на ключ словаря, разумеется), независимо от вашей базы данных. Такие заполнители будут заменены параметрами из аргумента params.

Примечание

Параметры в виде словаря не поддерживаются с бэкендом SQLite; с этим бэкендом вам нужно передавать параметры как список.

Предупреждение

Не используйте форматирование строк в сырых запросах или не заключайте заполнители в кавычки в ваших SQL-строках!

Соблазнительно написать вышеприведённый запрос так:

>>> query = 'SELECT * FROM myapp_person WHERE last_name = %s' % lname
>>> Person.objects.raw(query)

Вы также можете подумать, что ваш запрос должен выглядеть так (с кавычками вокруг %s):

>>> query = "SELECT * FROM myapp_person WHERE last_name = '%s'"

Не делайте ни одной из этих ошибок.

Как обсуждалось в защите от SQL-инъекции, использование аргумента params и оставление заготовок без кавычек защищает вас от атак SQL-инъекции, распространённого способа взлома, где злоумышленники вводят произвольный SQL в вашу базу данных. Если вы используете интерполяцию строк или заключаете placeholder в кавычки, вы подвергаетесь риску SQL-инъекции.

Выполнение произвольного SQL непосредственно

Иногда даже Manager.raw() недостаточно: вам может потребоваться выполнить запросы, которые не соответствуют моделям, или напрямую выполнить запросы UPDATE, INSERT, или DELETE.

В таких случаях вы всегда можете получить прямой доступ к базе данных, полностью обойдя слой модели.

Объект django.db.connection представляет собой подключение к базе данных по умолчанию. Для использования подключения к базе данных вызовите connection.cursor() для получения объекта курсора. Затем вызовите cursor.execute(sql, [params]) для выполнения SQL и cursor.fetchone() или cursor.fetchall() для возврата полученных строк.

Например:

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

Чтобы защититься от SQL-инъекции, не заключайте заполнители %s в кавычки в строке SQL.

Обратите внимание, что если вы хотите включить в запрос символы процента, вам нужно удвоить их в случае передачи параметров:

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 для получения подключения (и курсора) для определённой базы данных. django.db.connections — это похожий на словарь объект, который позволяет получить конкретное подключение с использованием его псевдонима:

from django.db import connections
with connections['my_db_alias'].cursor() as cursor:
    # Your code here...

По умолчанию, Python DB API возвращает результаты без имен полей, что означает, что вы получаете list значений, а не dict. С небольшой потерей производительности и памяти вы можете возвращать результаты как dict, используя что-то вроде этого:

def dictfetchall(cursor):
    "Return all rows from a cursor as a dict"
    columns = [col[0] for col in cursor.description]
    return [
        dict(zip(columns, row))
        for row in cursor.fetchall()
    ]

Другой вариант — использовать collections.namedtuple() из стандартной библиотеки Python. namedtuple — это объект, подобный кортежу, поля которого доступны с помощью поиска по атрибуту; он также индексируется и итерируется. Результаты неизменяемы и доступны по именам полей или индексам, что может быть полезно:

from collections import namedtuple

def namedtuplefetchall(cursor):
    "Return all rows from a cursor as a namedtuple"
    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))

>>> cursor.execute("SELECT id, parent_id FROM test LIMIT 2");
>>> dictfetchall(cursor)
[{'parent_id': None, 'id': 54360982}, {'parent_id': None, 'id': 54360880}]

>>> cursor.execute("SELECT id, parent_id FROM test LIMIT 2");
>>> results = namedtuplefetchall(cursor)
>>> results
[Result(id=54360982, parent_id=None), Result(id=54360880, parent_id=None)]
>>> results[0].id
54360982
>>> results[0][0]
54360982

Подключения и курсоры

connection и cursor в основном реализуют стандартный Python DB-API, описанный в PEP 249, — за исключением обработки обработки транзакций.

Если вы не знакомы с Python DB-API, обратите внимание, что SQL-запрос в cursor.execute() использует плейсхолдеры, "%s", а не добавляет параметры непосредственно в SQL. Если вы используете этот метод, подлежащая база данных автоматически экранирует ваши параметры по мере необходимости.

Также обратите внимание, что Django ожидает плейсхолдер "%s", а не плейсхолдер "?", который используется связками SQLite Python. Это сделано для поддержания согласованности и стабильности.

Использование курсора как контекстного менеджера:

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

эквивалентно:

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

Вызов хранимых процедур

CursorWrapper.callproc(procname, params=None, kparams=None)

Вызывает хранимую процедуру базы данных с указанным именем. Может быть предоставлена последовательность (params) или словарь (kparams) входных параметров. Большинство баз данных не поддерживают kparams. Из встроенных бэкендов Django только Oracle его поддерживает.

Например, для хранимой процедуры в базе данных Oracle:

CREATE PROCEDURE "TEST_PROCEDURE"(v_i INTEGER, v_text NVARCHAR2(10)) AS
    p_i INTEGER;
    p_text NVARCHAR2(10);
BEGIN
    p_i := v_i;
    p_text := v_text;
    ...
END;

Это вызовет её:

with connection.cursor() as cursor:
    cursor.callproc('test_procedure', [1, 'test'])
Изменено в Django 2.0:

Добавлен аргумент kparams.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/2.1/topics/db/sql/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API