Сегодня был опубликован план на следующий релиз Джанго 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
Вообще говоря, подобная фича уже много лет существует в SQLAlchemy. Причем там она реализована (на мой вкус) намного приятней и красивей: http://www.sqlalchemy.org/docs/05/ormtutorial.html#datamapping_querying_common
Все же, я думаю, ORM - одно из самых слабых мест Django.
Ни про какой "прорыв" тут конечно речь не идет. К сожалению, у SQLAlchemy нет нормального веб-фреймворка вокруг :-). ORM-то — не самоцель.
А вообще, для django это весьма классная фича.
Плюсовики не дадут соврать, что низкая скорость комиляции — одна из проблем языка. Не фатальная нисколько, но всем бы хотелось, чтобы было побыстрее.
Иван, да, к моему большому сожалению :(
TurboGears 2.0 все еще в слишком зачаточном состоянии, а 1.х - тупиковая ветка; Pylons оочень уж медленно развивается. А больше и не могу припомнить вреймворков, в которые был бы интегрирован этот замечательный ORM (я имею в виду SQLAlchemy).
А откуда взялось название "F"?
Очевидно, "Field".
Ура, я это знал :) (узнал, когда попытался найти в Python lvalue функции).
А Django 1.0 не патчится, чтобы было можно работать с F?
Про 1.0 не знаю (причем тут она?), а вот на транковую версию патч из тикета ложиться просто обязан.
Мы сейчас используем 1.0, но F хорошая штука.
А какая религия не позволяет использовать что-то дальше 1.0? Учитывая, что ты все равно собираешься накладывать на нее патч, который даже в транк еще не попал, и ни дня не тестировался у кого-либо кроме его автора?
На самом деле, я думаю, что патч на 1.0 вполне встанет. Да и сам в транк попадет в районе считанных недель. Хотя я часто ошибаюсь про джанговские сроки.
Пожуём, увидим :)
Знаю, что окажусь сильно в меньшинстве, но я в ORM ничего хорошего не вижу. IMHO это излишняя вещь, причём из разряда дырявых абстракций. В простых задачах ORM юзать можно, но в простых задачах без него проще (или, как минимум, не хуже), чем с ним. А на сложных задачах ORM создаёт больше проблем, чем решает.
Что касается Django, IMHO зря туда ORM встроили, да ещё так капитально. Большинство (если не все) фич, которые работают в Django "magically" благорадя ORM, можно было реализовать и без ORM. Впрочем, я Django смотрел не глубоко, так что здесь могу сильно ошибаться.
Если Иван сподобится написать статью "в общем" по ORM, безотносительно Django - там можно будет подискутировать более детально. :)
Болк, мы можем начать использовать trunk, если захотим.
Да нет уже в джанге практически никакой магии. Не нужно продолжать плодить мифы.
ну наконец то. хотя, конечно, синтаксически, способ, которым это сделано в sqlalchemy / stom - приятнее.
Arikon, ты тут как оказался?)
Меня, конечно, обвинят в ангажированности, но мне подход Алхимии не нравится принципиально (хотя и не сильно). Там приходится постоянно для указания полей повторять имя "главной" таблицы. В Джанго как-то сразу признали, что у большинства запросов такое понятие фактически есть, и от него и пляшут. Алхимия же делает вид, что в запросе все таблицы равны.
На sql-уровне Алхимии указание таблицы действительно практикуется (обоснованно), но там зачастую можно воспользоваться шорткатом:
F = lambda c: sqlalchemy.sql.column(c)
На уровне orm в Алхимии и вовсе используется объект Query с методом filter_by(), которому указание "главной" таблицы уже не требуется (в этом просто нет необходимости).
Иван, явное указание таблицы имеет смысл, причем очень большой. Если делается выборка со сложными джойнами или, скажем, с агрегированием, в запросе в любом случае фигурируют более одной таблицы. А о таких случаях дао Python говорит нам, что "explicit is better than implicit" :)
Кстати, описанная ситуация традиционно решается в Django написанием сырого SQL, что само по себе не очень хорошо.
Ну причем тут дао? Его можно поворачивать как душе угодно. Модель queryset'а ничем не менее "explicit"...
Кстати, агрегация в этой версии тоже уже будет. Фактически этот F-объект — ее побочный продукт :-).
ORM наиболее слабая часть Django. В 1.0 разработчики решили, что любой join должен содержать только два поля в ON части, и с каждым обновлением добавляются новые ограничения. Как кто-то здесь правильно упоминал дырявые абстракции. Многие проекты достигают такой стадии, когда сложность внутреннего устройства, обрекает их на умирание и единственный выход - все выбросить и инаписать с нуля. Феномен Django ORM в том, что эта стадия достигннута в 1.0 релизе.
Просмотрев внутренности Django 1.0 ORM, я для себе решил, новые database-intensive проекты на Django пока не начинать, буду пробовать sqlalchemy + paste.
Дима, а давайте вы напишете про это подробней где-нибудь пост? Я очень не хочу разводить здесь флейм из краткого комментария, где на основании одной экзотической претензии ("join только по двум полям") делается вывод о то, что ORM — наиболе слабая часть Джанго, не подходящая для database-intensive проектов. Я честно не понял, как это связано, и что такое конкретно "database intensive". Кроме того, считать, что джанговский ORM дошел до нынешнего состояния, постепенно усложняясь — это неверно по факту.
Я хотел бы поговорить про это все, но вот не в рамках комментариев к этому посту.
Интересно, когда в джанге появится поддержка group by? Или я что-то пропустил и она уже есть?
Это агрегация. Она будет в этой же версии 1.1.
Хотя жаль... Появится легкий способ писать тяжелые запросы, вместо того, чтобы денормализовывать данные.
Интересно. В дот нет есть LINQ с похожим функционалом, только его можно применять и для объектов в памяти и для БД(LINQToSql, LinqToEntities) и вобще для чего угодно, например для рест сервисов.
Хрень какая-то. Какой смысл в функции-костыле 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)
Наверное можно было... Хотя тогда бы была путаницу между атрибутами класса Page и также названными атрибутами объектов page. Плюс еще есть Page._meta.get_fields('count'), который тоже поле — но именно объект поля, а не проксик типа F. Кроме того, пришлось бы все время повторять имя класса, что просто некрасиво.
А это просто синтаксическая ошибка в Питоне. Слово до "=" должно быть идентификатором.
Ну не всегда денормализация спасет от аггрегации. Вот, допустим, мне недавно понадобилось сделать такое: есть таблица с логами (две колонки — айди объекта и таймстамп), нужно из нее выбрать 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)
Какие у вас предложение есть как обойтись без аггрегации?
ORM - это круто!
ясное и простое:
select * from employee where current_salary > start_salary;
заменяется на фантастическое уродство:
Employee.objects.filter(current_salary__gt=F('start_salary'))
Емплойи обжектс филтер СКОБКИ ОТКРЫВАЮТСЯ каррент сэлэри андерскор андерскор ГЭ ТЭ иквэл ЭФ СКОБКИ ОТКРЫВАЮТСЯ КАВЫЧКИ ОТКРЫВАЮТСЯ старт сэлэри КАВЫЧКИ ЗАКРЫВАЮТСЯ СКОБКИ ЗАКРЫВАЮТСЯ СКОБКИ ЗАКРЫВАЮТСЯ
на что только не идут люди, чтобы избежать SQL.
Это такой типичный strawman, что даже забавно :-). Откуда вы вообще это берете — "чтобы избежать SQL"? Да, может ради этого и делались вещи, когда в ORM'ы люди только начинали играться лет 20 назад, но сейчас это даже не обсуждается.
Из джанговских принципов http://docs.djangoproject.com/en/dev/misc/design-philosophies/#option-to-drop-into-raw-sql-easily-when-needed:
Вот, если хотите, пара причин прямо из головы того, зачем нам нужен этот синтаксис:
Вы правда так читаете весь код?
P.S. И еще забавно, что ваш кусок SQL в точности того же размера, что и джанговский код :-)
жду недождусь этого патча в транке :)
а ничего что ваш sql возвращает список строк, а строка на django - объект? Абсолютно некорректное сравнение.
Уже в транке.
Кстати ацкие оптимизаторы используют ORM для одноразовой генерации SQL запросов, т.е. чтобы не придумывать сложные запросы на убогом языке, чтобы это сделала машина.
А потом используют всю скорость традиционного SQL в свою пользу.
Соответственно, в новой версии запросы перегенерируются.
Но по-моему это бред. Дешевле добавить десяток ядер и сдать проект быстрее.