Сегодня был опубликован план на следующий релиз Джанго 1.1. В нем много всякого хорошего, но мое внимание привлекла красота реализации одной из must-have фич, и я решил поделиться с вами неожиданно нахлынувшей на меня радостью :-). Заодно кто-то, как я, узнает новую для себя возможность Питона.

Речь идет о тикете 7210 — "Added expression support for QuerySet.update". Это про то, что сейчас джанговский ORM не умеет создавать SQL для условий в запросах, в которых есть а) выражения:

update page set views_count = views_count + 1 where id = 123;

б) поля в качестве значений:

select * from employee where current_salary > start_salary;

Теперь это можно будет делать.

Загвоздка

Но самое интересное — это синтаксис, которым такие штуки записываются. В нем, собственно, и была главная загвоздка, почему этой фичи не было в ORM еще давно. Действительно, если написать все как есть, то получится:

# вместо увеличения на единицу текущего значения Питон увеличит локальну переменную 
# views_count, которой еще и нет наверняка
Page.objects.filter(pk=123).update(views_count=views_count + 1)

# вместо поля start_salary будет использована строка "start_salary", которая свалится
# на приведении к числу
Employee.objects.filter(current_salary__gt='start_salary')

F-объект

Проблему решили введением специального объекта, который означает "поле в этом запросе". И назвали его, что важно, кратко — F. В итоге оба примера начинают выглядеть вот так:

Page.objects.filter(pk=123).update(views_count=F('views_count') + 1)
Employee.objects.filter(current_salary__gt=F('start_salary'))

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

Минуточку...

Раз F — объект для отложенного хранения имени поля, то как работает конструкция F('views_count') + 1? Оказывается, F переопределяет для себя все имеющие смысл математические операции методами __add__, __sub__, __mul__ и прочими, которые Питон использует при рассчете выражений. Результатом каждой из них является такой же объект F, который хранит у себя свои операнды. Соответственно, эти вызовы можно комбинировать (хотя практического юзкейса я еще не придумал):

op = F('field') + 1   →  op = F('field').__add__(1)
op2 = op * 3          →  op2 = op.__mul__(3)

А вот на закуску стоит сказать, что эти вещи работают и при переставленных операндах. То есть:

1 + F('field')

... не свалится, хотя у объекта int и не определено, как к нему __add__ джанговский F-объект. Вот эта магия достигается за счет обратных методов операций: __radd__, __rmul__ и т.д. Они вызываются у правого объекта в операции, если вызов прямого метода на левом объекте выдал ошибку NotImplemented. Соответственно:

1 + F('field')        →  F('field').__radd__(1)

Я даже, пожалуй, скажу, что такой простой двухступенчатый алгоритм нравится мне больше, чем дико сложная штука в C++, где выражение x + y будет пытаться найти все возможные преобразования типов операндов, позволяющих выполнить операцию.

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

  1. General

    Вообще говоря, подобная фича уже много лет существует в SQLAlchemy. Причем там она реализована (на мой вкус) намного приятней и красивей: http://www.sqlalchemy.org/docs/05/ormtutorial.html#datamapping_querying_common

    Все же, я думаю, ORM - одно из самых слабых мест Django.

  2. Иван Сагалаев

    Ни про какой "прорыв" тут конечно речь не идет. К сожалению, у SQLAlchemy нет нормального веб-фреймворка вокруг :-). ORM-то — не самоцель.

  3. Сергей Кищенко
    • Но при этом в С++ все намного гибче получается. Да и примеры покруче - например, можно добавить в С++ динамичность.
    • Скорость поиска всех возможных преобразований не так уж и волнительна, ведь это будет происходить лишь на этапе компиляции

    А вообще, для django это весьма классная фича.

  4. Иван Сагалаев

    Плюсовики не дадут соврать, что низкая скорость комиляции — одна из проблем языка. Не фатальная нисколько, но всем бы хотелось, чтобы было побыстрее.

  5. General

    Иван, да, к моему большому сожалению :(

    TurboGears 2.0 все еще в слишком зачаточном состоянии, а 1.х - тупиковая ветка; Pylons оочень уж медленно развивается. А больше и не могу припомнить вреймворков, в которые был бы интегрирован этот замечательный ORM (я имею в виду SQLAlchemy).

  6. FX Poster

    А откуда взялось название "F"?

  7. Иван Сагалаев

    Очевидно, "Field".

  8. bolk

    Ура, я это знал :) (узнал, когда попытался найти в Python lvalue функции).

    А Django 1.0 не патчится, чтобы было можно работать с F?

  9. Иван Сагалаев

    Про 1.0 не знаю (причем тут она?), а вот на транковую версию патч из тикета ложиться просто обязан.

  10. bolk

    Мы сейчас используем 1.0, но F хорошая штука.

  11. Иван Сагалаев

    А какая религия не позволяет использовать что-то дальше 1.0? Учитывая, что ты все равно собираешься накладывать на нее патч, который даже в транк еще не попал, и ни дня не тестировался у кого-либо кроме его автора?

    На самом деле, я думаю, что патч на 1.0 вполне встанет. Да и сам в транк попадет в районе считанных недель. Хотя я часто ошибаюсь про джанговские сроки.

  12. bolk

    Пожуём, увидим :)

  13. Powerman

    Знаю, что окажусь сильно в меньшинстве, но я в ORM ничего хорошего не вижу. IMHO это излишняя вещь, причём из разряда дырявых абстракций. В простых задачах ORM юзать можно, но в простых задачах без него проще (или, как минимум, не хуже), чем с ним. А на сложных задачах ORM создаёт больше проблем, чем решает.

    Что касается Django, IMHO зря туда ORM встроили, да ещё так капитально. Большинство (если не все) фич, которые работают в Django "magically" благорадя ORM, можно было реализовать и без ORM. Впрочем, я Django смотрел не глубоко, так что здесь могу сильно ошибаться.

    Если Иван сподобится написать статью "в общем" по ORM, безотносительно Django - там можно будет подискутировать более детально. :)

  14. arikon.livejournal.com

    Болк, мы можем начать использовать trunk, если захотим.

  15. Boo

    Большинство (если не все) фич, которые работают в Django "magically"

    Да нет уже в джанге практически никакой магии. Не нужно продолжать плодить мифы.

  16. barbuza

    ну наконец то. хотя, конечно, синтаксически, способ, которым это сделано в sqlalchemy / stom - приятнее.

    Arikon, ты тут как оказался?)

  17. Иван Сагалаев

    Меня, конечно, обвинят в ангажированности, но мне подход Алхимии не нравится принципиально (хотя и не сильно). Там приходится постоянно для указания полей повторять имя "главной" таблицы. В Джанго как-то сразу признали, что у большинства запросов такое понятие фактически есть, и от него и пляшут. Алхимия же делает вид, что в запросе все таблицы равны.

  18. Андрей Стромнов

    Там приходится постоянно для указания полей повторять имя "главной" таблицы.

    На sql-уровне Алхимии указание таблицы действительно практикуется (обоснованно), но там зачастую можно воспользоваться шорткатом:

    F = lambda c: sqlalchemy.sql.column(c)
    

    На уровне orm в Алхимии и вовсе используется объект Query с методом filter_by(), которому указание "главной" таблицы уже не требуется (в этом просто нет необходимости).

  19. General

    Иван, явное указание таблицы имеет смысл, причем очень большой. Если делается выборка со сложными джойнами или, скажем, с агрегированием, в запросе в любом случае фигурируют более одной таблицы. А о таких случаях дао Python говорит нам, что "explicit is better than implicit" :)

    Кстати, описанная ситуация традиционно решается в Django написанием сырого SQL, что само по себе не очень хорошо.

  20. Иван Сагалаев

    Ну причем тут дао? Его можно поворачивать как душе угодно. Модель queryset'а ничем не менее "explicit"...

    Кстати, агрегация в этой версии тоже уже будет. Фактически этот F-объект — ее побочный продукт :-).

  21. Dima Dogadaylo

    ORM наиболее слабая часть Django. В 1.0 разработчики решили, что любой join должен содержать только два поля в ON части, и с каждым обновлением добавляются новые ограничения. Как кто-то здесь правильно упоминал дырявые абстракции. Многие проекты достигают такой стадии, когда сложность внутреннего устройства, обрекает их на умирание и единственный выход - все выбросить и инаписать с нуля. Феномен Django ORM в том, что эта стадия достигннута в 1.0 релизе.

    Просмотрев внутренности Django 1.0 ORM, я для себе решил, новые database-intensive проекты на Django пока не начинать, буду пробовать sqlalchemy + paste.

  22. Иван Сагалаев

    Дима, а давайте вы напишете про это подробней где-нибудь пост? Я очень не хочу разводить здесь флейм из краткого комментария, где на основании одной экзотической претензии ("join только по двум полям") делается вывод о то, что ORM — наиболе слабая часть Джанго, не подходящая для database-intensive проектов. Я честно не понял, как это связано, и что такое конкретно "database intensive". Кроме того, считать, что джанговский ORM дошел до нынешнего состояния, постепенно усложняясь — это неверно по факту.

    Я хотел бы поговорить про это все, но вот не в рамках комментариев к этому посту.

  23. Николай

    Интересно, когда в джанге появится поддержка group by? Или я что-то пропустил и она уже есть?

  24. Иван Сагалаев

    Это агрегация. Она будет в этой же версии 1.1.

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

  25. hodzanassredin

    Интересно. В дот нет есть LINQ с похожим функционалом, только его можно применять и для объектов в памяти и для БД(LINQToSql, LinqToEntities) и вобще для чего угодно, например для рест сервисов.

  26. semenov

    Хрень какая-то. Какой смысл в функции-костыле F('field'), когда можно было использовать для этого Model.field?

    ср.:

    Page.objects.filter(pk=123).update(count=F('count') + 1)
    Page.objects.filter(pk=123).update(count=Page.count + 1)
    

    либо для унификации:

    Page.objects.filter(pk=123).update(Page.count=Page.count + 1)
    
  27. Иван Сагалаев
    Page.objects.filter(pk=123).update(count=Page.count + 1)
    

    Наверное можно было... Хотя тогда бы была путаницу между атрибутами класса Page и также названными атрибутами объектов page. Плюс еще есть Page._meta.get_fields('count'), который тоже поле — но именно объект поля, а не проксик типа F. Кроме того, пришлось бы все время повторять имя класса, что просто некрасиво.

    Page.objects.filter(pk=123).update(Page.count=Page.count + 1)
    

    А это просто синтаксическая ошибка в Питоне. Слово до "=" должно быть идентификатором.

  28. Николай

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

    Ну не всегда денормализация спасет от аггрегации. Вот, допустим, мне недавно понадобилось сделать такое: есть таблица с логами (две колонки — айди объекта и таймстамп), нужно из нее выбрать 3 последних уникальных объекта. SELECT DISTINCT здесь не справится, потому что отсортировать по времени уже не выйдет. Получился вот такой вот код (на sqlalchemy):

    s = select([models.Log.c.object_id])
    s = s.group_by(models.Log.c.object_id)
    s = s.order_by(func.max(models.Log.c.timestamp).desc())
    s = s.limit(3)
    

    Какие у вас предложение есть как обойтись без аггрегации?

  29. perfectionist with deadlines

    ORM - это круто!

    ясное и простое:
    select * from employee where current_salary > start_salary;

    заменяется на фантастическое уродство:
    Employee.objects.filter(current_salary__gt=F('start_salary'))

    Емплойи обжектс филтер СКОБКИ ОТКРЫВАЮТСЯ каррент сэлэри андерскор андерскор ГЭ ТЭ иквэл ЭФ СКОБКИ ОТКРЫВАЮТСЯ КАВЫЧКИ ОТКРЫВАЮТСЯ старт сэлэри КАВЫЧКИ ЗАКРЫВАЮТСЯ СКОБКИ ЗАКРЫВАЮТСЯ СКОБКИ ЗАКРЫВАЮТСЯ

    на что только не идут люди, чтобы избежать SQL.

  30. Иван Сагалаев

    на что только не идут люди, чтобы избежать SQL.

    Это такой типичный strawman, что даже забавно :-). Откуда вы вообще это берете — "чтобы избежать SQL"? Да, может ради этого и делались вещи, когда в ORM'ы люди только начинали играться лет 20 назад, но сейчас это даже не обсуждается.

    Из джанговских принципов http://docs.djangoproject.com/en/dev/misc/design-philosophies/#option-to-drop-into-raw-sql-easily-when-needed:

    The database API should realize it’s a shortcut but not necessarily an end-all-be-all. The framework should make it easy to write custom SQL – entire statements, or just custom WHERE clauses as custom parameters to API calls.

    Вот, если хотите, пара причин прямо из головы того, зачем нам нужен этот синтаксис:

    • абстракция от синтаксических особенностей конкретной СУБД
    • возможность программно составлять разные варианты запросов из гарантированно корректных кусков, а не с помощью ненадежной конкатенации строк
    • бесплатная защита от SQL injection

    ... СКОБКИ ОТКРЫВАЮТСЯ ...

    Вы правда так читаете весь код?

    P.S. И еще забавно, что ваш кусок SQL в точности того же размера, что и джанговский код :-)

  31. krevedko

    жду недождусь этого патча в транке :)

  32. Kirax

    ясное и простое:
    select * from employee where current_salary > start_salary;
    заменяется на фантастическое уродство:
    Employee.objects.filter(current_salary__gt=F('start_salary'))

    а ничего что ваш sql возвращает список строк, а строка на django - объект? Абсолютно некорректное сравнение.

  33. Boo

    Уже в транке.

  34. Сергей Шепелев

    на что только не идут люди, чтобы избежать SQL.

    Кстати ацкие оптимизаторы используют ORM для одноразовой генерации SQL запросов, т.е. чтобы не придумывать сложные запросы на убогом языке, чтобы это сделала машина.

    А потом используют всю скорость традиционного SQL в свою пользу.
    Соответственно, в новой версии запросы перегенерируются.

    Но по-моему это бред. Дешевле добавить десяток ядер и сдать проект быстрее.

Добавить комментарий