8 марта 2012 г.

Новинки Django 1.4, day 1: ORM плюшки


Продолжение серии постов «Новинки Django 1.4».

Сегодня мы расскажем про те новинки, которые появились в компоненте ORM. Многие недолюбливают Django ORM за его простоту, которая мешает разработчикам составлять сложные SQL-запросы. С другой стороны нам трудно отрицать изящность API, превращающую большое число однотипных несложных запросов в читабельные QuerySet.

В Django 1.4 появилось сразу 3 новых метода:
  • bulk_create()
  • select_for_update()
  • prefetch_related()

Model.objects.bulk_create

Docs
Commit 16739

Полезная штука, позволяет вставлять в базу сразу несколько объектов в один запрос, например так:

Вместо N запросов:
Entry.objects.create(headline="Python 3.0 Released")
Entry.objects.create(headline="Python 3.1 Released")
...
Entry.objects.create(headline="Python 3.3 Planned") 

Теперь можно сделать тоже самое за один запрос:

Entry.objects.bulk_create([
    Entry(headline="Python 3.0 Released"),
    ...
    Entry(headline="Python 3.3 Planned") 
])

Плюс очевиден: это работает быстрей за счёт уменьшения количества запросов. Минусы в том, что функция save() для моделей не вызывается, сигналы pre_save и post_save не возбуждаются. Кроме того, метод не работает с мульти-табличным наследованием.

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



QuerySet.select_for_update()

Docs
Commit 16058

Реализация запросов вида SELECT ... FOR UPDATE для джанговского ORM. Запрос вида

entries = Entry.objects.select_for_update().filter(author=request.user)

создаст эксклюзивную блокировку на запись строк выборки до конца текущей транзакции. Если строки уже заблокированы другой транзакцией, запрос будет ждать её завершения, чтобы получить свою блокировку.  Чтобы избежать этого поведения, можно использовать необязательный атрибут nowait=True, который либо тут же без ожидания создаст блокировку, либо вызовет DatabaseError, если это невозможно.

Будьте внимательны, аргумент nowait не поддежривается бэкендом MySQL и вызовет всё тот же DatabaseError.

Функциональность будет полезна тем, кто желает создавать гарантированные row locks для изменения записей в базе данных в рамках транзакции, например, при обсчёте биллинга, увеличении счётчиков и т.п.

Реализация SELECT ... FOR UPDATE в PostgreSQL
Реализация SELECT ... FOR UPDATE в MySQL


QuerySet.prefetch_related

Docs
Commit 16930

Наверное, самая полезная фича сегодняшнего обзора. Она помогает радикально сократить число запросов, создаваемых через ORM без использования всяческих костылей (на DjangoCon Europe 2011 предлагались решения типа djanngo-unjoinify и django-queryset-transform).

Обычно для сокращения числа запросов используется метод .select_related(), который добавляет к запросу необходимые JOIN'ы. Несмотря на все его плюсы (указание глубины связей, а также полей, которые необходимо достать), этого явно недостаточно. select_related работает только со связями типа 1-M и 1-1 (ForeignKey и OnetoOne) и не поддерживает M-M. Данный пробел заполняет новый prefetch_related, который делает JOIN не в SQL, а на уровне питон-кода, добавляя по одному запросу на каждую связанную сущность.

Пример оптимизации из документации

#models.py
class Topping(models.Model):
    name = models.CharField(max_length=30)

class Pizza(models.Model):
   name = models.CharField(max_length=50)
   toppings = models.ManyToManyField(Topping)

def __unicode__(self):
   return u"%s (%s)" % (self.name, u", ".join([topping.name
            for topping in self.toppings.all()]))

#views.py
def show_me_some_pizzas():
    render_to_response(‘template.html’, {‘objects’:Pizza.objects.all()})


Выводя в шаблоне {{ pizza }} из вычисленного QuerySet, мы получим по дополнительному запросу на каждую пиццу, что совсем не круто (для 20 пицц в БД выходит 1+20=21 запрос). На помощь приходит prefetch_related:

#views.py
def show_me_some_pizzas_fixed():
render_to_response(‘template.html’, {‘objects’:Pizza.objects.all().prefetch_related('toppings')})


Данный вид+шаблон сделают всего два запроса (при этом результаты обоих будут полностью загружены в память, в отличие от максимально ленивого QuerySet, который будет хранить одновременно в памяти миниально необходимый набор строк).

Функциональность prefetch_related этим не ограничивается, можно делать “псевдо-джойны” с птичьей нотацией:

Restaurant.objects.prefetch_related('best_pizza__toppings')

и даже совмещать их с настоящими джойнами через select_related

Restaurant.objects.select_related('best_pizza').prefetch_related('best_pizza__toppings')


Более подробно читайте в доках prefetch_related


To be continued...
Завтра будет продложение про безопасность и формы

Комментариев нет:

Отправить комментарий