Django ORM — медленный? Оптимизируем (хардкорно)

    Наступила осень, и, несмотря на хорошую погоду, наши зрители потянулись за новым видеоконтентом. Бекенд-серверы, обслуживающие эмбеды с видео, стали упираться в CPU. С криками "а-а-а" прибежали системные администраторы и начали отбирать у отдела разработки ноутбуки и десктопы, грозясь поставить их в датацентры "на усиление". Разработке это конечно всё не понравилось и все поувольнялись с этой хренью решили что-нибудь сделать.


    В общем-то, решений в хайлоад-мире известно немало, начиная от диких "давайте перепишем всё самое сложное на Go" и заканчивая банальным кэшированием всего. Однако на этот раз речь пойдет про ускорение того, что не перепишешь и не закэшируешь — бизнес-логики процесса разрешения показа видео.


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


    В результате, для каждого конкретного просмотра необходимо сделать с десяток запросов к MySQL, отформатированный вариант которых не помещается на экране. Запросы идут к небольшим таблицам, выполняются быстро, однако узким местом является формирование таких запросов с использованием Django ORM. Посидев с полчаса с профайлером и посмотрев на результаты, автор выяснил две вещи. Первое — что использование django-rest-framework в настолько сложном API подталкивает программиста к копипасте и повторным вызовам одних и тех же функций в рамках одного запроса. Рекорд — 6 раз разобрать URL реферера на поддомены и сегменты пути. Второе — что внутри Django ORM действительно медленный проходит до половины времени обработки запроса.


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


    str.format()


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


    SQLAlchemy + Baked Queries


    Смена ORM на другой, более быстрый и со встроенной поддержкой кэширования, могла бы стать отличным вариантом, если бы этим заморочились на старте проекта. А так, несмотря на наличие тестов, встает вопрос об эквивалентности переписанного кода. Baked Queries из SQLAlchemy позволяют вместо построения запроса с нуля использовать кэш в памяти процесса, чтобы переиспользовать однажды отформатированный SQL-запрос при повторных вызовах с другими параметрами. Эта идея была одной из наиболее "вкусных", пока не появился вариант, ставший причиной публикации данной статьи.


    Кэш для Django ORM


    Понятно, что внедрить кэширование внутрь Django ORM не получится, но что если попробовать закэшировать уже отформатированный SQL-запрос? В теории, выглядит это очень просто:


    queryset = get_some_complex_sql(flag1=True, flag2=True)
    sql, params = queryset.query.sql_with_params()
    raw_queryset = Model.objects.raw(sql, params=params)

    Берем технику memoization, модифицируем так, чтобы в расчет брались не конкретные значения, а их типы, наличие/отсутствие (ну ладно, (True, False, None, 0, 1) можно тоже учитывать). Запоминаем SQL, в случае cache hit подставляем новые значения в RawQuerySet и вот наш кэш готов.


    На практике, проблемы начинаются почти сразу. params — это всегда просто кортеж встроенных типов python, и отличить, где в нем flag1, а где flag2 — невозможно. Помимо этого, любой фильтр типа filter(value__in=[1,2,3]) модифицирует SQL-запрос в зависимости от числа переданных в фильтр значений, а это ведет к комбинаторному взрыву ключей кэширования.


    Запасшись безумным количеством кофе и печенек, проблему соответствия фактических параметров формальным удалось решить путем обхода внутренних структур Query и поиска, где там в них подставляется очередной параметр. Получилась работающая, но ужасающая по сложности конструкция.


    Мне стыдно за этот код...
    @cached(
        'play_qualityrule',
        # локальные поля модели
        licensed=not_null_and_negate('licensed'),
        protected=not_null_and_negate('protected', default=False),
        is_active={'exact': True},
        # QualityRule.rightholders.filter(rightholder_id=...)
        rightholders__rightholder_id=null_or_equal('rightholder'),
        # QualityRule.user_agents.filter(useragent_id=...)
        user_agents__useragent_id=null_or_equal('user_agent'),
        # QualityRule.groups.filter(group=...)
        groups__group_id=null_or_in('group'),
        # QualityRule.alternative_sales_rule.filter(
        #     alternativesalesrule=...)
        alternative_sales_rule__alternativesalesrule_id=null_or_equal(
            'alternative_sales_rule'),
        # QualityRule.users.filter(user_id=...)
        users__user_id=null_or_equal('user'))
    def get_filtered_query(self, **kwargs):
        ....

    То есть по сути любой параметр, участвующий в построении SQL-запроса, должен был быть описан в декораторе вместе с правилами его формирования относительно аргументов, переданных в функцию. Это нагромождение кода не устраивало сложностью в начальной реализации и последующей поддержке.


    Promise и ленивые вычисления


    Следующий подход был навеян декоратором django.utils.functional.lazy. Работает он следующим образом.


    def compute(param):
        return param ** 2
    
    compute_lazy = lazy(compute, int)
    
    lazy_value = compute_lazy(43)

    Если lazy_value передать в Django ORM, то до самого последнего момента вызов функции compute() будет отложен; функция будет вызываться каждый раз, когда будет производиться явное приведение объекта Promise к реальному значению.


    Это свойство было задействовано для получения фактических значений параметров из контекст-менеджера.


    def lazy_param(name):
        return ContextManager.instance.params[name]

    Следующая идея была наивно простой:


    • оборачиваем все параметры функции в lazy-прокси
    • кэшируем QuerySet
    • ...
    • профит!

    К сожалению, не сложилось. Оказалось, что Django-ORM для каких-то своих грязных целей производит приведение типов и проверку длин списков еще на этапе конструирования QuerySet: например, в вызове filter(a__in=[1,2,3]) производится проверка на пустое значение, и соответствующая нода, вместо добавления нового условия в WHERE, бросает EmptyResultSet, типа для оптимизации запроса, зараза.


    Соответственно, ни для каких ленивых вычислений тут уже места не остается.


    Lazy!


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


    • При приведении к str/unicode возвращается LazyUnicode
    • При приведении к int возвращается LazyInt
    • При попытках итерации возвращается итератор с одним элементом — LazyList.

    За счет всей этой "лени" удалось довести Lazy-обертки до завершения формирования SQL-запроса, попутно решив проблему с переменным числом placeholder-ов у проверки на вхождение в список.


    sql, params = "SELECT * FROM bla WHERE a IN (%s) AND b = %s", ([1,2,3], 4)
    
    placeholders = get_placeholders(params)
    sql = sql % placeholders
    params = flatten(params)
    
    # SELECT * FROM bla WHERE a IN (%s, %s, %s) AND b = %s", (1, 2, 3, 4)

    При подстановке фактических параметров выражение IN (%s) заменяется на IN (%s, %s, %s) с числом плейсхолдеров, соответствующих реальному числу значений в списке, а кортеж params делается плоским.


    Теперь код, использующих кэширование, выглядит намного изящнее.


        @cached
        def get_filtered_query(self, **params):
            ...
            return queryset
    
        def useful_method(self, **params):
            with LazyContext(**params):
                qs = self.get_filtered_query(**params)
                # query database
                return list(qs)

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


    Какова цена?


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


    @cached
    def get_queryset(user):
        if user and user.is_authenticated():
            return Model.objects.exclude(author=user)
        return Model.objects.filter(public=True)

    Видно, что SQL-запрос зависит двух фактов:


    • в функцию передан пользователь
    • этот пользователь авторизован.

    То, что в функцию передан пользователь, декоратор отследить в состоянии; однако проверять авторизацию не в его компетенции, как, например, и то, что ID этого пользователя является простым числом. Такие случаи ветвлений, основанные на вызовах методов у переданных объектов, на дополнительных запросах к БД и состоянии глобального контекста функции (да хоть datetime.now()) — это всё необходимо выносить за скобки. К счастью, правила для этого достаточно просты:


    • не передавать объекты моделей (только ID),
    • не передавать пустых списков (None вместо них),
    • не использовать значения, которые нельзя вычислить по переданным аргументам.

    Пример выше изменяется совсем чуть-чуть.


    @cached
    def get_queryset(user_id, is_authenticated):
        # проверяем фактическое значение параметра, а не Lazy-обертку на истину
        if reveal(is_authenticated):
            return Model.objects.exclute(author_id=user_id)
        return Model.objects.filter(public=True)
    
    def caller(user):
        if user and user.is_authenticated():
            user_id = user.pk
            is_authenticated = True
        else:
            user_id = None
            is_authenticated = False
        with LazyContext(user_id=user_id, is_authenticated=...):
            qs = get_queryset(user_id, is_authenticated)
            return list(qs)

    Вместо заключения


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


    • На старте боевой трафик зеркалируется на отдельный бекенд, с целью сравнить результаты работы оригинальной и модифицированной версии кода
    • В тестах используется "параноидальный" режим работы, когда наряду с "ленивой" реализацией работает "нормальная" версия кода, и если SQL-запросы или их параметры не соответствуют друг другу, это считается ошибкой
    • Более вменяемый режим сверяет эталонную реализацию с ленивой в момент сохранения результата в кэш, а в случае расхождений отключает кэширование
    • Наконец, набор feature-флагов позволяет нам быстро отключить кэш SQL-запросов для конкретного метода, если мы заметим, что код работает неверно.

    Что касается эффективности, то на синтетических прогонах ab-шкой одного и того же запроса удалось ускориться с 31 до 44 запросов в секунду. Результат получен во-первых, для специфической бизнес-логики, во-вторых, в дико синтетической ситуации, в-третьих, на первой попавшейся машине. И что немаловажно, с учетом округления, мы получили ускорение на 42%.


    Реализация кэша запросов доступна на GitHub.

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Как вам такой подход?

    Поделиться публикацией

    Похожие публикации

    Комментарии 25

      0

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

        +1
        Если обратиться к лидирующему на данный момент ответ с str.format, чисто вскидку, если это возможно прикинуть, эффективность была бы выше, как вы думаете?
          0

          Это вообще похоже наиболее эффективный вариант, круче только прямое обращение к MySQL по HTTP за JSON-ами. Вот только в наших условиях переписать на str.format около тысячи (на вскидку) возможных вариантов SQL-текстов будет затруднительно.

            –1

            Приведу еще аргумент против str.format.


            как это вообще переписать возможно?
            SELECT DISTINCT
              "paywall_alternativesalesrule"."id",
              "paywall_alternativesalesrule"."priority",
              "paywall_alternativesalesrule"."is_active",
              "paywall_alternativesalesrule"."name",
              "paywall_alternativesalesrule"."player_stub_id",
              "paywall_alternativesalesrule"."type",
              "paywall_alternativesalesrule"."watch_everywhere",
              "paywall_alternativesalesrule"."provider_id",
              "paywall_alternativesalesrule"."licensed",
              CASE WHEN ("paywall_alternativesalesrule_os"."os_id" IS NULL AND
                         "paywall_alternativesalesrule_subnet"."subnetgroup_id" IS NULL AND
                         ("acl_domain"."name" IN (%s, %s, %s) OR
                          "paywall_alternativesalesrule_allow_domains"."domain_id" IS NULL)
                         AND (NOT (T11."name" IN (%s, %s, %s) AND T11."name" IS NOT NULL) OR
                              "paywall_alternativesalesrule_deny_domains"."domain_id" IS
                              NULL))
                THEN %s
              ELSE %s END AS "is_targeted"
            FROM "paywall_alternativesalesrule"
              LEFT OUTER JOIN "paywall_alternativesalesrule_videos"
                ON ("paywall_alternativesalesrule"."id" =
                    "paywall_alternativesalesrule_videos"."alternativesalesrule_id")
              LEFT OUTER JOIN "paywall_alternativesalesrule_os"
                ON ("paywall_alternativesalesrule"."id" =
                    "paywall_alternativesalesrule_os"."alternativesalesrule_id")
              LEFT OUTER JOIN "paywall_alternativesalesrule_subnet"
                ON ("paywall_alternativesalesrule"."id" =
                    "paywall_alternativesalesrule_subnet"."alternativesalesrule_id")
              LEFT OUTER JOIN "paywall_alternativesalesrule_allow_domains"
                ON ("paywall_alternativesalesrule"."id" =
                    "paywall_alternativesalesrule_allow_domains"."alternativesalesrule_id")
              LEFT OUTER JOIN "acl_domain"
                ON ("paywall_alternativesalesrule_allow_domains"."domain_id" =
                    "acl_domain"."id")
              LEFT OUTER JOIN "paywall_alternativesalesrule_deny_domains"
                ON ("paywall_alternativesalesrule"."id" =
                    "paywall_alternativesalesrule_deny_domains"."alternativesalesrule_id")
              LEFT OUTER JOIN "acl_domain" T11
                ON ("paywall_alternativesalesrule_deny_domains"."domain_id" = T11."id")
            WHERE ("paywall_alternativesalesrule"."is_active" = % s AND
                   ("paywall_alternativesalesrule"."licensed" IS NULL OR
                    "paywall_alternativesalesrule"."licensed" = % s) AND
                   ("paywall_alternativesalesrule_videos"."video_id" IS NULL OR
                    "paywall_alternativesalesrule_videos"."video_id" = % s) AND
                   ("paywall_alternativesalesrule"."watch_everywhere" = % s OR (
                     "paywall_alternativesalesrule_os"."os_id" IS NULL AND
                     "paywall_alternativesalesrule_subnet"."subnetgroup_id" IS NULL AND
                     ("acl_domain"."name" IN (%s, %s, %s) OR
                      "paywall_alternativesalesrule_allow_domains"."domain_id" IS NULL) AND
                     (NOT ("paywall_alternativesalesrule"."id" IN
                           (SELECT U1."alternativesalesrule_id" AS Col1
                            FROM "paywall_alternativesalesrule_deny_domains" U1 INNER JOIN
                              "acl_domain" U2 ON (U1."domain_id" = U2."id")
                            WHERE U2."name" IN (%s, %s, %s))) OR
                      "paywall_alternativesalesrule_deny_domains"."domain_id" IS NULL))))
            ORDER BY "paywall_alternativesalesrule"."priority" ASC, "is_targeted" DESC
              0
              Написать хранимую процедуру?

              Кстати, вот такие IN (%s, %s, %s) с переменным количеством %s бьют уже по самой СУБД. Потому что СУБД тоже пытается кешировать планы запросов. А каждое изменение количества параметров в запросе приводит к созданию нового плана запросов и к тому же самом комбинаторному взрыву. Правильнее было бы передавать один параметр — массив значений.
                0

                Не кажется ли вам, что трансляция существующей бизнес-логики из Django в хранимые процедуры это, мягко скажем, безумие? Даже если не брать в расчет декодирование idna-доменов и прочие штуки, для которых нет SQL-функций MySQL, получается перенос нагрузки на CPU с бекенда на базу данных.

            0
            «не передавать объекты моделей (только ID)» — вообще в Python объекты передаются по ссылке поэтому нет смысла передавать айдишники.
              0

              Вообще не про то разговор) Передав в фунцию объект модели Django, можно повытаскивать у этого объекта связанные ForeignKey-объекты, повызывать методы разные и т.п. — и навертеть в генерируемом SQL-запросе каких угодно новых условий так, что кэширующий декоратор этого не заметит.
              Получается очень подверженный ошибкам подход, поэтому речь идет о прокидывании значений первичных ключей БД, которые к передаче по ссылке не имеют никакого отношения.

              0
              Не думаю, что стоимость получившейся разработки вышла намного дешевле стоимости апгрейда серверов… кроме того, увеличились вероятности ошибок в связи с нестандартным поведением (грубо говоря, где-то забыл передать id, а передал по-старинке объект, тест прошел, а аукнулось это намного позже). Я бы тут больше подумал об оптимизации бизнес логики, возможно там слишком наверчено, что это приводит к таким вот зубодробительнм запросам…

              Хотя тут может быть все решает политика руководства — типа платить за новые сервера дорого и с чего бы это. А программистам вроде как и так зарплата платится, вот пусть чинят свои косяки…
                0

                1 человеко-месяц понадобился в данном случае. Это примерно четверть от стоимости одного нового сервера.

                0
                В вашем случае этот метод не приемлемый, но вдруг пробовали…
                Что будет если оптимизнуть сам Python в этом месте, например на PyPy запустить?
                  0

                  Не знаю, поддерживает ли PyPy Django в полной мере сейчас, давно не смотрел в эту сторону. Но перетянуть с полсотни зависимостей на другой интерпретатор будет довольно сложно.

                    0
                    Да, на таком проекте я бы не рискнул так делать для боевых задач,
                    но в познавательных целях было-бы интересно посмотреть на результат.
                    На моих задачах подобного рода прирост скорости был примерно в 5 раз. При этом, что удивительно, потребление памяти оказалось в 2 раза меньше, чем из под CPython.
                  +1
                  В принципе, решение вполне нормальное. Для каждого достаточно большого проекта подход для оптимизации может быть своим.
                  Кстати, если бы использовались CBV, то просто так декоратор для кеширования не всегда бы мог решить проблему. Если внутри кроме аргументов есть что-то типа self.object или другие свойства, то они могут быть не учтены при таком кэшировании.

                  выражение IN (%s) заменяется на IN (%s, %s, %s) с числом плейсхолдеров, соответствующих реальному числу значений в списке, а кортеж params делается плоским.

                  Тут еще вопрос что будет быстрее… Я бы лучше оставил один плейсхолдер, а параметр передавал как
                  ",".join([str(i) for i in ids])
                  . Быстрее чем подставлять плейсхолдеры в строке. Минус — вы теряете проверку на безопасность аргументов, но если они не напрямую переданы с веба, то почему бы и нет?

                  однако узким местом является формирование таких запросов с использованием Django ORM

                  После беглого взгляда на реализацию возникает вопрос — а сильно ли меньше кода получилось со всеми этими Lazy классами? Метрикам верю, но не сложнее ли вышло?
                    +1

                    Насчет Class-Based Views. Декоратор навешивается на метод менеджера модели, хотя это не обязательно. Получается, что всё кэширование остается на уровне работы с моделями, не задевая вьюшки и остальной код. Таким образом почти всё, касающееся Manager.self, можно объявить безопасным в плане учета в ключе кэширования. Понятно, что накосячить можно везде, от всего не защитишься.


                    Насчет замены в выражении IN проще показать код (и да, мы передаем в запрос разобранное значение заголовка реферер — так что str не катит).


                    Всё крайне просто
                    def normalize(sql, params):
                        placeholders = []
                        real_params = []
                        for p in map(reveal, params):
                            if isinstance(p, (list, tuple)):
                                # IN(%s) -> IN(%s,%s,%s)
                                placeholders.append(', '.join(['%s'] * len(p)))
                                real_params.extend(p)
                            else:
                                placeholders.append('%s')
                                real_params.append(p)
                        return sql % tuple(placeholders), tuple(real_params)

                    Насчет объема кода. В каждом таком месте, где вызывался метод построения СЛОЖНОГО queryset-а, пришлось добавить контекст-менеджер и разнести построение queryset и итерацию по нему в разные методы. В среднем получилось плюс пять строк на каждый метод, и куча рефакторинга по приведению передаваемых параметров в "безопасный" для кэширования вид.

                    0

                    Может и не уместно, но не могу промолчать. У джанги не только ОРМ медленный, и шаблонизатор, и даже простой json другие фреймворки отдают быстрее. Модульная архитектура быстрее, функциональнее, гибче. Flask, bottle, Jinja2, Babel, Beaker, WTForms и т.д. и т.п. А если без ОРМ совсем никак, то SQLAlchemy. Печально, что в вебе такой язык как питон в первую очередь ассоциируется с джангой. Сугубо ИМХО.

                      +1
                      Странный подход. Если будете рыть большую яму, то воспользуетесь лопатой? Нет. Воспользуетесь экскаватором. Так и в разработке, каждый фреймворк заточен под определенный класс задач.
                        0

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

                          +1
                          Опять какой-то странный подход: взял, собрал из отдельных компонентов и сразу все производительно стало. Не бывает так. Django надо уметь готовить и уметь проектировать приложения для того чтобы достичь максимальной производительности. Она не хуже будет работать чем скопище отдельных компонентов.

                          Сколько раз не сталкивался с проектами основанными на множестве отдельных компонентов с Flask внутри, все болеют одной и той же проблемой
                          — велосипедостроительством. Как-только проект набирает массу кода, то он начинает превращаться в набор костылей с сложно отлаживаемой логикой. Переписывание проекта на Django спасает проект, и код, в этом случае, получается стандартным, в котором сможет разобраться любой нормальный программист Python.

                          Flask и ему подобные хороши для маленьких проектов с простой бизнес логикой, который разрабатывается командой в 2-3 человека. Где больше трех уже стоит использовать Django.

                          Все это испытано на своей шкуре и не на одном проекте.
                            0

                            Не сказал бы что Django прям вот любой костылепроект спасает, но порядка добавляет — это точно.

                              0
                              Согласен, что не любой. Можно и в Django такого наворотить, что скупая слеза навернется, а вот убраться и привести все в порядок в таком проекте намного легче чем на основе множества компонентов.

                              Еще чем лучше Django других фреймворков это предсказуемость цикла поддержки и времени жизни версии. Это для бизнеса очень веское преимущество. Ни каких революций и ломания API. Все предсказуемо, задокументировано и по плану.
                              0
                              Django надо уметь готовить и уметь проектировать приложения для того чтобы достичь максимальной производительности.

                              Любой проект нужно уметь проектировать правильно. Только вот как ты не крути, компоненты джанги от этого быстрее не станут, например шаблонизатор, орм… Они тормознутые своей реализацией.


                              Вот и автор статьи пишет, что отличным вариантом было бы отказаться от ОРМ джанги в пользу иного. Но пришлось решать проблему костылями. А по уму так большинство компонентов джанги следует заменить, ибо уступают аналогичным решениям. И в итоге от неё останется микро-фреймворк с кучей мусора.


                              Сколько раз не сталкивался с проектами основанными на множестве отдельных компонентов с Flask внутри, все болеют одной и той же проблемой — велосипедостроительством. Как-только проект набирает массу кода, то он начинает превращаться в набор костылей с сложно отлаживаемой логикой.

                              Что конкретно приходилось велосипедостроить вместе с фласком, но не нужно было c джангой? Если речь идёт не о 5ти строчек кода?


                              Когда возникает необходимость отклониться от монолитности, универсальности джанги, когда она делает не так как нам надо, когда нам нужно расширить её функционал тут как раз и возникают костыли. Flask и др. предоставляют простоту, гибкость и аккуратность в работе, позволяя пользователю самому выбирать, как реализовать те или иные вещи.


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

                              Да, джанго код стандартен. Пока нам ничего не нужно больше стандартного или немного иного. И разберется в нём джанго программист, а не "лубой нормальный python программист". Нормальный питонист не значит джангист и наоборот. :)


                              Flask и ему подобные хороши для маленьких проектов с простой бизнес логикой, который разрабатывается командой в 2-3 человека. Где больше трех уже стоит использовать Django.

                              Чем больше проект, тем больше будет требований по гибкости и нагрузкам. То есть не джанго. CMS -> Django -> Flask и др. -> python


                              А вообще так спорить можно до бесконечности. Давайте остановимся на этом. :) Каждому своё.

                                0
                                А вообще так спорить можно до бесконечности. Давайте остановимся на этом. :) Каждому своё.


                                Да, конечно. Это самое разумное решение в таком споре.
                        +1
                        Ну, раз уж столько критики обрушилось в сторону Django, то поделюсь ссылкой на эту статью о проблемах Django и способах их решения.

                        Прекомпиляцию запросов использовал часто, но django queryset для этого обычно не использовал. Не потому что его трудно кэшировать, а просто потому, что с ним хватает проблем и помимо этого. Django прекрасно понимает Raw-SQL, а это значит, что ответственность за создание SQL-запроса вовсе не обязательно возлагать на Django.

                        Обычно фабрику каждого запроса лучше оформлять отдельным классом, тогда его реализацию можно легко и безболезненно подменить. Внутри может быть как обычный Raw-SQL (что, правда, тоже не лишено определенных проблем, но эти проблемы не выходят за рамки одного класса), так и какой-нибудь высокоуровневый билдер запросов. Приложению это не важно, приложение не должно проникать внутрь границ инкапсуляции. И желательно все-таки детали обращения к источнику данных скрывать хотя бы за сервисным слоем.

                        Если автору статьи больше импонирует билдер запросов алхимии, советую обратить внимание на sqlalchemy-django-query и на другие интеграционные библиотеки которые перечислены в указанной статье.

                        Django-Rest-Framework прекрасно интегрируется с SQLAlchemy на своем уровне, ссылки на интеграции тоже приведены в указанной статье.
                          0
                          Если Ваша команда высоко-квалифицированная в области архитектуры и проектирования, вы используете методики совместной разработки для распространения опыта, чувствуете в себе силы сделать проект более качественным без Django, и располагаете достаточными ресурсами и финансами для этого, тогда есть смысл использовать другой стэк технологий.

                          А мне понравилась эта цитата из приведенной статьи. Отлично обосновывает, почему именно Django используется в абсолютном большинстве наших проектов.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое