Сегодня был опубликован план на следующий релиз Джанго 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-уровне Алхимии указание таблицы действительно практикуется (обоснованно), но там зачастую можно воспользоваться шорткатом:
На уровне 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 и также названными атрибутами объектов page. Плюс еще есть Page._meta.get_fields('count'), который тоже поле — но именно объект поля, а не проксик типа F. Кроме того, пришлось бы все время повторять имя класса, что просто некрасиво.
А это просто синтаксическая ошибка в Питоне. Слово до "=" должно быть идентификатором.
Ну не всегда денормализация спасет от аггрегации. Вот, допустим, мне недавно понадобилось сделать такое: есть таблица с логами (две колонки — айди объекта и таймстамп), нужно из нее выбрать 3 последних уникальных объекта. SELECT DISTINCT здесь не справится, потому что отсортировать по времени уже не выйдет. Получился вот такой вот код (на sqlalchemy):
Какие у вас предложение есть как обойтись без аггрегации?
ORM - это круто!
ясное и простое:
select * from employee where current_salary >