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

Я покопался в Django и нашел N вещей, которые не нравятся в нем лично мне. Не претендую ни на объективную оценку их относительной важности, ни на полноту этого списка. Мне вообще кажется, что это больше всего повеселит поклонников каких-нибудь других фреймворков :-).

ORM не поддерживает агрегацию

Очень часто хочется сделать вещи типа "список всех статей с количеством комментариев к ним", "список юзеров с количеством сообщений от них" — то, что делается через "group by" и "count()". В джанговском языке запросов нет прямого средства для этого, и это очень плохо, потому что очень легко побуждает писать в шаблонах дико неэффективный код:

{% for article in articles %}
{{ article.subject }}, {{ article.comment_set.count }}
{% endfor %}

Он будет делать отдельный запрос с count на каждую строчку.

HTML escape не включен по умолчанию.

Идеология "не додумывать лишнего за пользователя" вышла боком в том, что в шаблонах все переменные по умолчанию выводятся без экранирования HTML. Писать в каждом месте {{ var|escape }} очень легко забывается, и приводит к появлению XSS-уязвимостей. Но самая главная проблема в том, что это сейчас невозможно починить просто, потому что уже написана куча кода, который работает в предположении, что этого нет, а значит будет делать двойное экранирование. Придется чинить сложно.

Авторизация завязана на модель User

Модель User всегда была предназначена для использования в админке. И только потом стало понятно, что сам процесс авторизации — это отдельная удобная штука, которая пригодится и за ее пределами. Авторизацию худо-бедно выдрали, и она даже подходит для общего использования, но она слишком многими вещами завязана на модель User, которую нельзя по-человечески расширить, и приходится пользоваться разными ухищрениями с профилями.

Conditional get бесполезен

Есть такая штука — ConditionalGetMiddleware. Она умеет обрабатывать клиентские запросы с заголовком If-Modified-Since и отдавать короткий ответ "Not Modified", если содержимое не менялось. Это очень эффективная техника кеширования динамических сайтов, которые нельзя кешировать на жесткие промежутки времени. Эффективна она, причем, не столько для клиента, сколько для сервера, который может вообще не генерировать тяжелый динамический контент, если известно, что он не менялся. Но эта middleware как раз этого и не дает сделать: проверка времени последнего изменения делается после безусловной генерации ответа. То есть клиента от перекачивания ненужных данных спасает, а сервер от их генерации — нет.

P.S. Хотя у меня есть идеи для Cicero, как сделать conditional get более интеллектуальным и полезным.

Байтовые строки по умолчанию

Родовая травма софта, написанного в ASCII-мире, не минула и Django. Он изначально был написан с использованием обычных байтовых строк вместо unicode, что означает появление периодических проблем с разными старыми кодировкам, upcase'ами, отсечением символов и подсчетом длины строки.

Однако! Некоторое время назад таки был создан UnicodeBranch, в котором Малколм Трединник переводит Django на внутренне использование юникода. Я там тоже периодически участвую. Бранч очень близок к завершению, поэтому я призываю всех интересующихся скачать его и потестировать на своих проектах.

Context в render_to_response

Render_to_response задумывался как удобный способ сократить пять строк в одну в очень частом случае возврата ответа из view. Случай действительно очень частый, и все им пользуются. И надо ж так было случиться, что эта удобная функция не использует RequestContext, который передает в шаблоны данные по умолчанию, и тоже используется практически всегда. В итоге, практически любой вызов render_to_response сопровождается либо дописыванием одного и того же лишнего параметра, либо порождает разные автоматизирующие решения. А хотелось бы, чтобы это было просто сделано правильно.

Create/Update_object принимают модель

Generic views — замечательная штука, которая позволяет не писать одинаковый код (вывод списка объектов, вывод одного объекта, вывод архива за год и т.п.) Create_object и update_object призваны автоматизировать обработку форм: вывод формы с начальными значениями, прием данных, валидацию, повторный вывод при ошибках, сохранение данных в базу. Все это они делают, достаточно только указать модель данных.

Однако на практике получается, что редко какая форма работает только с одной моделью. Она может принимать данные для нескольких моделей, может вообще не связываться с базой, а например отсылать письмо о начале регистрации... В итоге лично у меня во view полно этой самой обработки форм, практически одинаковой, только формы разные.

А вот если бы вместо модели эти generic view принимали класс формы, все бы было гораздо более шоколадно.

Upload через память

Ну и напоследок. С самых давних времен весь upload файлов в Django происходит с полной загрузкой этого файла в память. Для аватарок работает, для архивов с музыкой — не работает. Я в свое время правил это в своих проектах, но тот патч в Django не попал. Вместо него разрабатывается очень долго и трудно новое мега-решение, которое, к сожалению, пытается решать много проблем сразу вместо одной. Но его наверное все таки включат, потому что в итоге это нужно.

Вот. N получилось равным 8.

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

  1. Alexander Solovyov

    Хотел написать, что 7 - мне пофиг. А потом понял, что как раз не пофиг - я ж им потому и не пользуюсь. ;) А вообще да, всё поддерживаю, остальные все - у меня в моём списке недостатков есть. :)

  2. Виктор

    А "N вещей, которые я люблю в Django" будут? ;) или уже были?

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

    Вот они: http://softwaremaniacs.org/blog/category/django/

    Их там будет еще много :-).

  4. Marat Radchenko

    С вашего позволения посмотрю на этот пост с точки зрения Java.

    ORM не поддерживает агрегацию

    Hibernate имеет судя по всему лучший язык запросов (по крайней мере в мире Java). И агрегацию в том числе.

    HTML escape не включен по умолчанию

    JSPX.

    Conditional get бесполезен

    Аналогично, обрабатывать ручками.

    Авторизация завязана на модель User

    Ни на что не завязана, можно делать любую.

    Байтовые строки по умолчанию

    Внутри везде UTF, в некоторых местах при чтении извне по умолчанию используется системная кодировка, приходится явно задавать UTF.

    Upload через память

    Вот это очень странное решение. Первый раз вижу что такое вообще используется.

  5. Василий Ставенко

    Я бы добавил еще то что джанго не умеет делать это - http://code.djangoproject.com/wiki/SchemaEvolution.

  6. Alexander Solovyov

    С вашего позволения посмотрю на этот пост с точки зрения Java.

    Это просто глупо. Какой-такой Java? Статья про Python или что? Она про фреймворк, а не про ЯП.

    Ни на что не завязана, можно делать любую.

    О чём тут вообще речь? О Java'е, что ли? А почему было не подумать, что на Python'е можно тоже что угодно сделать, и это проблема только лишь Джанги?

  7. crash

    А в примера с комментариями для статьи select_related() разве не помогает?

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

    Нет, select_related выбирает только вверх по родительскому дереву. То есть если выбирать комментарии, то с помощью select_related можно выбрать в одном запросе и их статьи, с которыми они связаны. Принципиально здесь то, что эта выборка не увеличит размер выборки комментариев, потому что у одного комментария не может быть много статей.

    В обратную же сторону, когда нужно выбрать статьи, если их просто помножить на таблицу комментариев, то в запросе статей станет больше, они будут повторяться (столько раз каждая, сколько у ней комментариев). Чтобы этого не происходило, надо делать группировку по статьям, а этого джанговский db-api делать не умеет.

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

    А почему было не подумать, что на Python’е можно тоже что угодно сделать, и это проблема только лишь Джанги?

    Давайте без крови :-). Мнение человека из Java-мира всегда ценно, потому что мнение питоновского мира мы и сами знаем.

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

  10. dark-demon

    Я б так жить не смог :)

    1. как раз за что не люблю орм - сначала решаешь головоломку, как что-то на нём реализовать, а наконец реализовав, - понимаешь, что всё это дело тормозит.
    2. а что, для питона/джанги не существует альтернативных темплейтных движков?
    3. убийственно :)
  11. crash

    А как тогда выходить из ситуаций с агрегацией, писать самому SQL запрос?
    Дело в том, что мне как раз пример с комментариями надо будет сделать и я вот призадумался, точнее, для этого конкретного все же надо кол-во комментариев хранить в таблице самих статей, как это рекомендует MySQL cookbook, но общий вопрос интересен.

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

    Я обычно после получения списка объектов из queryset'а делаю вручную еще один запрос с "group by", который отдает IDшки и count'ы. Потом каждому объекту из первого запроса приписываю в цикле эти count'ы. Получается два запроса вместо одного, но это все равно O(1) вместо O(n).

  13. undebugger

    Вообще, ORM в django очень примитивный, предназначенный для какого-то узкого класса задач. Мне почти всегда проще написать прямо sql-запрос; организовать результаты запроса в объекты - дело пары строк.

  14. undebugger

    2crash: да, брать и писать SQL-запрос. SQL - это ведь язык (запросов), и я вообще с трудом понимаю, зачем на язык громоздить ещё один язык (API моделей).

    SQLAlchemy в этом смысле, по крайней мере, честно говорит: внутре - SQL.

    А единственная причина использовать API моделей - это использование готовых частей django, которые используют API моделей (и, как заметил Иван в (3) и (7), делают это не совсем хорошо и правильно).

  15. dacuan

    К сожалению не имею опыта работы с Django, но новые технологии всегда интересны. Меня смутили два пункта из Вашего перечня:

    HTML escape не включен по умолчанию.

    Минус ли это? Попробую описать ситуацию.

    Предположим, что в шаблон передается строка со следующим содержанием:

    "<a>Click Me</a>
     <a>Click Me</a>"
    

    Обратите внимание, что текст многострочный. По вашему убеждению внутри шаблона, где бы она не встречалась эта последовательность символов должна быть автоматически заменена на:
    "Click Me
    Click Me"

    На первый взгляд все правильно. Но давайте представим, что этот текст должен выводится внутри alert() и что получится?
    alert("Click Me
    Click Me")

    Этот код, естественно, вызовет ошибку JavaScript. На самом деле строка должна была быть экранирована так:

    alert("<a>Click Me</a>\\n     <a>Click Me</a>")
    

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

    Но и такой "умный" обработчик не справится с ситуацией, когда переменная содержит HTML, который должен быть отображен "как есть" (например, новость, набранная в WYSIWYG-редакторе).

    Conditional get бесполезен

    Вы намекнули, что у вас есть решение этой проблемы. Было бы интересно узнать подробности.

  16. wiz

    Про render_to_response очень жаль, что ему надо RequestContext постоянно подсовывать, ведь HttpResponse это и есть ответ на HttpRequest.

    И про агрегацию тоже не понятно. Ведь даже в газетных сайтах наверняка нужно что-нибудь считать.

  17. Александр

    2 undebugger: ORM позволяет абстрагироваться от какой-то конкретной версии хранилища данных. Если ты пишешь, скажем, только для MySQL, то можно и вручную запросы писать, а если собираешься поддерживать несколько СУБД?

  18. buriy

    Спасибо за отличную статью.

  19. Maximbo

    зачем на язык громоздить ещё один язык (API моделей).

    Чтобы отделить мух от котлет — питон-код от кода SQL. Не знаю как вам, а мне значительно приятнее писать на чистом языке, без примесей.

    SQLAlchemy в этом смысле, по крайней мере, честно говорит: внутре - SQL.

    Да и Django тоже не заставляет делать всё через свой ORM. Менеджеры моделей — очень хорошая штука.

    P.S. кстати, может быть, после братания Django и SQLAlchemy возможности работы с БД станут гибче?

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

    HTML escape не включен по умолчанию.

    Минус ли это? Попробую описать ситуацию.

    Я веду речь только об экранировании по умолчанию. Оно должно быть HTML'ным, потому что это самый частый случай, и потому что не делать этого небезопасно. Но средства отключить экранирование или сделать другое экранирование быть, безусловно, тоже должны.

    Conditional get бесполезен

    Вы намекнули, что у вас есть решение этой проблемы. Было бы интересно узнать подробности.

    Я напишу подробно, когда реализую, но вкратце — это должен быть декоратор для view, принимающий отдельную функцию для быстрого взятия времени обновления или ETag'а.

  21. Mourner

    Думаю, самое слабое место в Django на сегодняшний день - это всё-таки schema migrations.

  22. dacuan

    Я веду речь только об экранировании по умолчанию.
    Оно должно быть HTML’ным, потому что это самый
    частый случай, и потому что не делать этого
    небезопасно. Но средства отключить экранирование
    или сделать другое экранирование быть, безусловно,
    тоже должны.

    Учитывая нынешнюю моду на динамические страницы, у разработчика остается возможность "забыть" о правильном экранировании данных, передаваемых в JavaScript и получить тот-же самый XSS, только через скриптовый язык. Если же добавить экранирование, то это будет только способствовать "привыканию" к тому, что "умный фреймворк делает все за нас" и заботиться о безопасности нет никакой потребности.

    Я напишу подробно, когда реализую,

    Очень интересно будет почитать.

  23. viestards

    Интересно, со многим согласен.
    По поводу #7, думаю, это стделать не сложно, generic views всо равно нужно переписать под newforms, но судя по тому, что разработчикам не нравится backwards- compatible изменения, этого не будет.

  24. pythy

    Спасибо за статью. Со всем изложенным в ней полностью согласен. Про schema evolution не сказал ;-)

  25. [...] Сагалаев написал про 7 вещей, которые я не люблю в Django. Я предлагаю эту тему развить и написать каждому о тех [...]

  26. mike

    Вот послушайте еще одно мнение Java-разработчика, правда бывшего, а теперь пересевшего на Python. Всякие там хибернейты приходят и уходят, а реляционные базы остаются. Мне, например, было удобнее работать с iBatis, чем с Hibernate как раз именно потому, что там можно было просто написать какой нужно SQL, вместо прыжков с бубном вокруг шибко умного API.

    Djangoвский ORM мне нравится за простоту и близость к БД. Проблему с агрегацией я решаю так: (в общем случае) создаю агрегирующий View, затем делаю запрос через ORM с такой добавкой:

    objects = Object.objects.filter( foo=bar ).extra(
      select={'sum1':'view.sum1', 'sum2':'view.sum2'},
      tables=['view'],
      where=['app_object.id=view.object_id']
    )
    

    Вариации: материализованный view, сабселект в параметре tables, ну.. много что еще можно придумать.

    Schema evolution - в бобруйск. Если у вас фреймворк принимает решения о структуре базы, то я вашей базе глубоко соболезную. Триггеры там всякие с хранимыми процедурами, куда и как они должны эволюционировать?

    А вот насчет HTML escape - это правда. Как хорошо в Zope это было сделано!

  27. vinya

    HTML escape не включен по умолчанию.

    А в формах для поля CharField нет возможности автоматически обрезать пробелы. Хорошо бы иметь параметр strip_spaes = True/False, который бы регулировал обработку поля. У меня в проекте, например, куча форм и нет ни одной где такая обработка не нужна. Задача настолько распространенная, что просто удивляет отсутствие подобного атрибута.

  28. Deepwalker

    Ну собственно я думаю вам теперь надо идти на сайт django, сделать патч и создать тикет, реализующий такую фичу. Его я думаю примут. Ну а мы, остальные, будем пользоваться.
    Это же не нечто глобально вроде escape или schema evolution.

  29. EvgIq

    Да, везде свои "тонкости" :)

    Я обычно после получения списка объектов из queryset'а делаю
    вручную еще один запрос с "group by", который отдает IDшки и
    count'ы. Потом каждому объекту из первого запроса приписываю в
    цикле эти count'ы. Получается два запроса вместо одного, но
    это все равно O(1) вместо O(n).

    Вообще все эти запросы с GROUP BY, по моему, не слабо сервак загружают.

    Честно говоря, давно не занимался программированием, но сейчас "приспичило".

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

    Обновлять содержимое этого поля буду при добавлении/удалении записей в подчиненной таблице (с комментами к статье, например). И только тут, для получения актуального количества комментариев (например), буду делать запрос.

    По моему плюсы:

    • Т.к. количество добавлений/удалений априори много < чем просмотров - сервак лучше "дышит"
    • Не надо морочиться с циклом ручного добавления количественного поля.

    Если в моих рассуждениях ошибка - поправьте :), буду благодарен.

  30. Михаил

    Иван, а могли бы вы составить подобный список для Django текущей версии? Чтобы знать, что ушло, а что осталось.

  31. ipomaranskiy@gmail.com

    Присоединяюсь к вопросу/просьбе Михаила. Я только начинаю изучать Django, и сейчас меня прямо очень мучает мысль «это всё слишком хорошо, чтобы быть правдой». :)

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