Spec-Zone.ru › Django 1.11

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

Когда API запросов модели не позволяют добиться желаемого результата, можно использовать SQL-запросы напрямую. Django предоставляет два способа выполнения таких запросов: вы можете использовать Manager.raw() для выполнения SQL-запросов и получения экземпляров моделей, или же обойти слой моделей и выполнить произвольный 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, обе строки будут соответствовать. Чтобы предотвратить это, выполните правильное приведение типов перед использованием значения в запросе.

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

Хотя экземпляр RawQuerySet можно перебирать, как обычный QuerySet, RawQuerySet не реализует все методы, которые можно использовать с QuerySet. Например, __bool__() и __len__() не определены в RawQuerySet, и поэтому все экземпляры RawQuerySet считаются True. Причина, по которой эти методы не реализованы в RawQuerySet, заключается в том, что их реализация без внутренней кэширования приведет к снижению производительности, а добавление такого кэширования будет несовместимо со старыми версиями.

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

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')
...

Сопоставление происходит по имени. Это означает, что вы можете использовать SQL-условия AS для сопоставления полей в запросе с полями модели. Таким образом, если у вас есть другая таблица, содержащая данные 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.

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

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

>>> 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.
...

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

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

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

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

Примечание

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

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

Не используйте форматирование строк в запросах raw!

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

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

Не делайте этого.

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

Выполнение произвольных 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

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

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
cursor = connections['my_db_alias'].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", а не "?", который используется связками Python с SQLite. Это сделано для согласованности.

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

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

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

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

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

CursorWrapper.callproc(procname, params=None)

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

Например, данная хранимая процедура в базе данных 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 Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/1.11/topics/db/sql/

Spec-Zone.ru

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