Нас Django вкус волнует и манит

    Кушаем кактус

    Прошло уже несколько недель, как официально вышла 3 версия Django. Я работал с этой версией ещё до публикации официального релиза и, к сожалению, заметил, что развитие Django сильно замедлилось. Версия 1.3 от 1.7 отличается в разы, а вот 3 версия содержит косметические изменения ветки 2 и не более.

    Мой проект winePad стартовал с версии Django 1.3, и к текущему моменту в нем переопределено около 12% внутреннего кода Django.

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

    Вот о работе над ошибками я и хочу рассказать:

    Метод get


    Мало кто догадывается о том, что в стандартном методе get django с самого начала была ошибка. Метод get должен вернуть вам либо один объект либо предупредить о том что найдено несколько объектов или же сообщить, что объектов нет.

    1  def get(self, *args, **kwargs):
    2         clone = self._chain() if self.query.combinator else self.filter(*args, **kwargs)
    3         if self.query.can_filter() and not self.query.distinct_fields:
    4             clone = clone.order_by()
    5         limit = None
    6         if not clone.query.select_for_update or connections[clone.db].features.supports_select_for_update_with_limit:
    7             limit = MAX_GET_RESULTS
    8             clone.query.set_limits(high=limit)
    9         num = len(clone)
    10        if num == 1:
    11            return clone._result_cache[0]
    12        if not num:
    13            raise self.model.DoesNotExist()
    14        raise self.model.MultipleObjectsReturned()
    

    Строка 9 получает данные по ВСЕМ записям которые указаны в queryset и переводит их в набор объектов. Об этом есть предупреждение в документации.

    До 3 версии ограничения на количество запрашиваемых объектов не было. Это означает, что get получал абсолютно все данные и превращал их в объекты, прежде, чем дать предупреждение что объектов много.

    В итоге вы могли получить несколько миллионов объектов в памяти, только для того, чтобы узнать, что найден более, чем 1 объект. Сейчас появились строки 5,7,8. И теперь вы гордо получите только MAX_GET_RESULTS=21 объект, прежде чем узнать, что объектов больше чем 1.
    При тяжёлом "__init__" задержка будет значительной. Как лечить:
    Переопределить MAX_GET_RESULTS в django.db.models.query.py
    переопределить GET или перед вызовом GET использовать:

    vars(queryset.query).update({'high_mark':2, 'low_mark':0})
    или
    queryset.query.set_limits(0,2)
    

    Метод __init__ моделей Django


    Не совсем понятно объявление в коде встроенного метода __init__

    _setattr=setattr

    вероятно, это для псевдоубыстрения кода переносом ссылки на функцию в локальный словарь, но речь не об этом. Проблем несколько:

    1. Если передать дополнительные пары аттрибут=значение в __init__ модели вы получите «got an unexpected keyword argument».

    В таком случае я предлагаю не утяжелять метод __init__ а делать добавление атрибутов после инициализации:

    obj = MyClass()
    vars(obj).update({'attr':val, 'attr2':val2 ...})
    

    2. В новой Django добавили возможность переопределения дескрипторов на любое поле модели (Field.descriptor_class). Но ни один дескриптор не знает, инициализирован объект, или ещё нет. Это надо, например, если дескриптор будет использовать данные из prefetch_related объектов, которые появятся только после инициализации главного объекта.

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

    В таком случае я не придумал ничего умнее, чем переопределить __init__ и добавить аттрибут окончания инициализации.

        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self._end_init = True
    

    Querysets


    Жестко прописанные EmptyQuerySet/DateQuerySet убраны, уже хорошо. Однако ситуация с queryset в роли менеджера мне идеологически не нравится.

    Если я хочу переопределить класс QuerySet создаваемых менеджерами, я добавляю атрибут _queryset_class

    class MyManager(models.Manager):
        _queryset_class = MyQuerySet
    

    Внимание, для старых версий это не работает, можно сделать, например, так:

    class MyManager(models.Manager):
        def get_query_set(self):
            response = super(MyManager, self).get_query_set()
            response.__class__ = MyQuerySet
            return response
    

    inlineFormset панели администратора


    Проблем несколько:

    1. Нельзя отобразить стандартным inlineFormset записи не имеющие напрямую связь с главным объектом формы. Например: форма правки цен товара. В середине лежит справочная статистическая Tabularinline форма закупочных оптовых цен на «подобный» товар.

    Решается переопределением метода get_formset inline модели и созданием собственного MyFormSet унаследованного от BaseInlineFormSet

    def get_formset(self, request, obj=None, **kwargs):
            kwargs['formset'] = MyFormSet
            super().get_formset(request, obj, **kwargs)
    
    class MyFormSet(BaseInlineFormSet):
        pass
    

    2. Если вы правите обьект с inlineformset в админ панели, а в это время кто-то удалит одну из записей обьектов внутри inlineformset через другой механизм, то вы получите ошибку и сохранить обьект не удастся. Только через kopy paste в новом окне браузера.

    Я нашел пока только одно Решение — не использовать inlineformset.

    Панель администратора


    «Киллер-фича» Django, является самым большим кактусом проекта:

    1. Действие «Удаление объектов» в администраторах моделей видны по умолчанию, не важно, есть ли права у пользователя на удаление, или нет.

    Решается отключением этого действия по умолчанию:

    admin.site.disable_action('delete_selected')
    

    2. Создание дополнительных прав пользователя из админ панели будет невозможно пока вы не включите администратор модели Permissions:

    from django.contrib.auth.models import Permission
    class PermissionsAdmin(admin.ModelAdmin):
    	search_fields = ('name', 'codename','content_type__app_label', 'content_type__model')
    	list_display = ('name', 'codename',)
    	actions = None
    admin.site.register(Permission, PermissionsAdmin)  
    

    3. Увы, прав на доступ только к определенным объектам в Django не существует.

    Это возможно решить через прописывание записи в djangoAdminLog со специальным флагом.
    А после проверять наличие флага:

    user.logentry_set.filter(action_flag=ENABLED, .....).exists()
    

    4. Если вы создаете действия администратора, так, как стоит в документации, то помните, что они не протоколируются в djangoAdminLog автоматически.

    5. Еще недостаток этой части документации — все примеры только на функциях. А как же GCBV? В моих проектах все действия администраторов моделей переведены на GCBV. Репозиторий.

    Подключение действия стандартно:

    class MyAdmin(admin.ModelAdmin):
        actions = (MyActionBasedOnActionView.as_view(),)
    

    ContentType — реестр моделей django


    50% гениальность / 50% тупость.
    Ни у одной модели нет доступа к реестру моделей по умолчанию.
    У нас в проектах решается добавлением миксина во все классы:

    from django.contrib.contenttypes.models import ContentType
    class ExportMixin(object):
        @classmethod
        def ct(cls):
            if not hasattr(cls, '_ct'):
                cls._ct, create = ContentType.objects.get_or_create(**cls.get_app_model_dict())
                if create:
                    cls._ct.name = cls._ct.model._meta.verbose_name
                    cls._ct.save()
            return cls._ct
    
        @classmethod
        def get_model_name(cls):
            if not hasattr(cls, '_model_name'):
                cls._model_name = cls.__name__.lower()
            return cls._model_name
    
        @classmethod
        def get_app_name(cls):
            if not hasattr(cls, '_app_name'):
                cls._app_name = cls._meta.app_label.lower()
            return cls._app_name
    
        @classmethod
        def get_app_model_dict(cls):
            if not hasattr(cls, '_format_kwargs'):
                cls._format_kwargs = {'app_label': cls.get_app_name(), 'model': cls.get_model_name()}
            return cls._format_kwargs
    

    теперь мы можем вызывать obj.ct() при необходимости.

    UserModel


    Возможность переопределения модели пользователя появилась в версии 1.5.

    Но к 3 версии так и не исправили model=User в стандартных UserCreationForm/UserChangeForm.
    Решается:

    from django.contrib.auth.forms import UserCreationForm
    from django.contrib.auth import get_user_model
    class MyUserCreationForm(UserCreationForm):
        class Meta:
            model= get_user_model()
    

    Система переводов


    Разметка текстов, видимых пользователю, выполняется тегами

    {% trans %}

    или в коде через

    gettext_lazy

    Однако, управление этими ресурсами в админпанели отсутствует. Вообще.

    Есть внешние решения, все они работают кое-как.

    Например, Rosetta систематически теряет тексты, и глючно работает интерфейс. Нигде нет проверки прав доступа к переводам. Для работы необходимы систематические makemessages / compilemessages…

    В winePad теги trans, blocktrans и gettext_lazy переопределены и мы стали получать тексты из кеша. Если нет кеша, то кешированный запрос из базы get_or_create избавил нас и от makemessages.

    Тема мультиязычности — вообще сложная. Встроенное в Django решение работает только для статических текстов. А ведь есть еще необходимость перевода данных моделей. Я попробовал по-своему решить вопрос перевода динамических текстов в проекте django-TOF, где я соединил возможности model-translation и Parler/Hvad. Вероятно, кому-то будет интересно заглянуть.

    Пока я остановлю повествование, поскольку статья по исправлению недостатков Django легко может превратится в longread.

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

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

    Подробнее
    Реклама

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

      0
      Метод get, возвращающий более 2х записей, да еще и миллионы, это такой редкий кейс ошибки, вы реально ради этого джангу поманчипатчили?)
        0
        Если судить по тому, что ограничение на количество получаемых объектов методом get уже попало в официальный релиз Django 3, то получается, что я не один такой сказочный долбо§б.

        Я уже в статье отметил, что считаю, что в этой функции ошибка. Ошибка в логике: функция должна получать только один объект, если он есть. Не два, не 25, и не два миллиона. Жаль только, что мой манкипатчинг (смешное слово) не исправляет эту ошибку а только уменьшает количество получаемых объектов.
          +1
          метод get действительно должен возвращать один объект, но это не значит что в sql должен быть limit 1. То, что можно получить несколько записей подстраховывает от ошибок, когда метод get выбирает по неуникальному кортежу. Если вы не хотите гарантировать эту уникальность, то берите .first()/.last(). Вы не будете получать ошибок в случае неправильно сформированных параметров запроса.
            +1
            По логике, данная функция должна инициировать и возвращать только один объект, и делать это только в том случае, если объект один.

            В реальной Django это невозможно сделать за один запрос. Потому текущая логика метода такова:
            • Проинициализировать объекты данными из запроса, будет проинициализировано столько объектов, сколько возвращено строк,
            • Объекты сохраняются в "_result_cache".
            • Если обьект один — Get вернет ссылку на первый и единственный объект в "_result_cache"
            • GET выдаст ошибку «DoesNotExist» если _result_cache пустой
            • GET вернет MultipleObjectsReturned, при этом "_result_cache" будет заполнен несколькими объектами и их количество не учитывается.


            В Django 3 появилось ограничение в 25 строк, это значит, что в sql запроса есть «LIMIT». Во всех предыдущих версиях этого ограничения не было вообще, и мы поставили у себя в проекте ограничение на количество возвращенных строк до 2х.
            В этом случае будут проинициированы максимум два обьекта. После чего Get выдаст ошибки, если получен иной результат, чем один объект.

            Ограничение на 2 объекта вместо 25 я предлагаю и для новой Django.
              0
              еще раз — если у вас .get() возвращает более 2х элементов, то это ошибка в коде.
              то что лимит в джанге добавили — это хорошо. но между 2 и 25 разницы особой нет, а вот миллионы да, могут стрельнуть
                0
                метод get возвращал и возвращает только один объект или выдает ошибки. Ни в коде, ни в моем тексте я не вижу упоминаний что get по окончанию возвращает что-то другое.

                прошу указать: где в коде или тексте статьи ошибка, подразумевающая, что результатом работы метода GET будет возврат двух и более объектов.
                  0
                  В итоге вы могли получить несколько миллионов объектов в памяти, только для того, чтобы узнать, что найден более, чем 1 объект.

                  вот же.
                  возвращает .get() один объект, но ему еще надо рейзить ошибку, когда из базы более 1 объекта прилетело, что свидетельствует об ошибке в логике программы.
                    0
                    Речь идет о методе GET в родном коде django (django/db/models/query.py). Я описал как он работает:
                    Объекты сохраняются в "_result_cache".
                    Если обьект один — Get вернет ссылку на первый и единственный объект в "_result_cache"
                    GET выдаст ошибку «DoesNotExist» если _result_cache пустой
                    GET вернет MultipleObjectsReturned, при этом "_result_cache" будет заполнен несколькими объектами и их количество не учитывается.
                    _result_cache хранится в памяти, методом GET не возвращается.
                      0
                      все верно. а чем противоречие?
                        0
                        Противоречие в том, что моя фраза «В итоге вы могли получить несколько миллионов объектов в памяти, только для того, чтобы узнать, что найден более, чем 1 объект.» не говорит, что результатом работы метода GET будет возврат двух и более объектов.

                        однако именно она была приведена с комментарием «вот же», как пример того, что результатом работы метода GET будет возврат более одного объекта.
                          0
                          Мне кажется изначально вам о другом говорилось, если у вас постоянно вылетает MultipleObjectsReturned, то надо не заниматься оптимизацией обработки этой ошибки, а правильно построить запросы или брать элемент при помощи .first().
                          Если get возвращает миллион записей а вы ожидаете одну(иначе почему get используется), то наверно не в get проблема
                            0
                            «Если get возвращает миллион записей...» — get возвращает один объект или ошибки. В процессе работы GET может создать много объектов на основе записей, возвращенных из базы.

                            Эта логическая ошибка в стандартном методе get заключается в том, что на 9 строке метода создаются объекты, которые, если их можно было создать больше одного — создаваться не должны были в принципе. А они создаются, и только потом проверяется их количество.

                            Кстати, когда выпал MultipleObjectsReturned, на обработке ошибки воспользоваться этими УЖЕ созданными «MultipleObjects» нельзя.
                              0
                              Я если честно ниче не понял, вы то пишите get, то GET. для меня это разные вещи, так как «GET» относится к методу get в CBV, а «get» это метод выборки в query. Приведите пример?

                              Вы делаете Human.objects.get(age__gt=20) и ругаетесь что django перед тем как выдать ошибку создает несколько миллиардов Human инстансев?
                              Да это плохо, да можно уже на втором бросить исключение, но так ваша цель получить какого-то человека а не ошибку, то наверно надо переписать либо условие выборки по уникальному id, либо брать случайного человека типа order_by('?').first()
                                +1

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


                                Но если мы пишем get — значит, иногда всё-таки исключение происходить должно. И видеть в этом "иногда" серьёзную деградацию производительности очень не хочется.

                                  0
                                  Спасибо.
                                    0
                                    Но если мы пишем get — значит, иногда всё-таки исключение происходить должно.

                                    В теории я могу такое допустить, хотя и не могу представить зачем. Можно пример? А еще лучше пример, когда у нас не просто несколько объектов, а тысячи объектов попадают под наше условие в фильтре get(). Что вообще за желание такое

                                    «Давайте использовать get и ловить MultipleObjectsReturned в нашей базе, зная что Human.objects.get(age__gt=20) будет очень сильно тупить и выуидывать нам исключение, давайте не использовать first() для таких случаев, будем страдать, ловить ошибки, но чтобы они были быстрей залезем в кишки джанго и кое чего перепишем, нам не важен корректный результат, нам нужно чтоб код работал быстрей пусть и с ошибками»

                                    я все правильно понял?
                                      0

                                      Скажите, а в каких случаях, по-вашему, использование get оправдано? Может, его надо вообще удалить из django?

                                        0
                                        Я ждал этого вопроса. Наличие get не принуждает нас использовать его везде где только можно.
                                        Оправдано оно там где мы ожидаем 1 единственный объект, в редких случаях 0 объектов и тогда мы превращаем это в 404 ошибку например. Т.е. выборка по уникальным полям, например номер статьи на хабре можно выбрать при помощи get, профиль пользователя по его id или юзернейму, почте и тд. Но я не представляю зачем умышленно брать get для выборки значений которые с большой вероятностью будут не уникальными, и их будет так много что эта ошибка съест приличное кол-во ресурсов.
                                        Следующий вопрос
                                        Может, его надо вообще удалить из django?

                                        Думаю джанго не пострадает если удалить его совсем. т.е. сейчас поздно удалять, слишком много где он присутствует, но в теории не вижу больших проблем обойтись filter().first() везде где используется get.
                                        С другой стороны get немного добавляет понимания, например если мы видим get, мы понимаем что объект в базе с такими параметрами фильтра должен быть один, и если их несколько то это исключительная ситуация. И либо выбран неправильный метод выборки либо у нас ошибка в базе — 2 пользователя с никнеймом swelf.

                                        Вообщем я так и не понял, зачем использовать get, если ожидается получение большого кол-во экземпляров модели. Зачем вообще ожидать несколько объектов при использовании get. «Извините у нас 2 или больше статьи на хабре с таким названием, поэтому мы вам не покажем ни одной»
                                          +1

                                          Ясно, с этого вам и надо было начинать, а не требовать объяснить разницу между 25 записями и 2мя когда вы не понимаете зачем вообще сам метод нужен...


                                          Так вот, есть такой принцип — fail fast. Он говорит, что программа, которая обнаружила ошибку в окружении или в самой себе, должна как можно быстрее сообщить об ошибке, чтобы её можно было исправить.


                                          Да, при этом появляется риск что программа поломается уже у пользователя — но он компенсируется тем, что при таком подходе больше багов будет отловлено на этапах отладки и тестирования.


                                          Рассмотрим ваши примеры. Вот у нас оказались два пользователя с ником swelf. Вы бы предпочли узнать об этом при первом же входе на хабр, или когда ваш двойник словит бан, и этот бан прилетит вам? Первое неприятно, но быстро решится через поддержку, а во втором случае ошибка может так и остаться незамеченной.

                                        0
                                        — я все правильно понял?
                                        — не всё.

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

                                          В коде метода get ошибка логики.

                                          Это я вижу, я думал что есть другие причины, помимо внутреннего перфекционизма исправлять это.
                                          mayorovp
                                          а не требовать объяснить

                                          ну не то чтоб требовал)
                                          Вот у нас оказались два пользователя с ником swelf. Вы бы предпочли узнать об этом при первом же входе на хабр

                                          А я и узнаю, точнее администратор ресурса узнает по ошибке. но в случае 2х пользователей будет ли такая дикая просадка по производительности, что ее кто-то заметит и будет смотреть, а что же там такое. Думаю возникнет вопрос, а как вообще так вышло, что у нас 2 пользователя с одним никнеймом, хотя казалось бы они должны быть уникальными. Вообщем о причинах уже написал ТС, просто перфекционизм)
                                          Так вот, есть такой принцип — fail fast

                                          Ну а в случае с методом get разве есть попытка избежать fail fast? Насколько я понимаю принцип говорит «При ошибке — падайте, не пытайтесь понять и исправить» Джанго и падает, просто падает медленно.

                                          Я тогда могу заметить, что есть понятие «преждевременная оптимизация», незачем переопределять метод get, если может быть когда-нибудь у нас будет такая архитектурная ошибка в базе данных, что исключение MultipleObjectsReturned сожрет у нас приличное кол-во ресурсов.
                                            0
                                            Джанго и падает, просто падает медленно.

                                            Принцип не просто так называется называется fail fast

            +2
            50% гениальность / 50% тупость.

            К сожалению вся джанга такая. Админка подходит только для стандартных задач, кастомизируется через жуткие костыли. Метод get вообще перестали использовать, только first. Да много чего хотелось бы поменять, поэтому вопрос, не пробовали пропихнуть свои изменения в саму джангу вместо очередной батарейки?
              0
              Админка подходит только для стандартных задач, кастомизируется через жуткие костыли.

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

                  Хм, в Django 3.0 же добавили поддержку асинхронности, разве это не большой шаг вперед? Я, правда, еще не смотрел как это выглядит, может быть это просто маркетинговый ход и работать с этим невозможно?
                    0
                    Правильней, сделали первый маленький шажок для добавления асинхронности, работы там дофига, есть целая статья рассказывающая что и как
                    0
                    По поводу замедления развития…
                    Когда у средней руки проекта пявляется минорный патч, он всегда выглядит революционно. Когда проект развился в нечто большое, минорными патчами уже никого не удивишь.
                    Django 3.0 — начато переписывание полностью синхронной Django под асинхронность. И уже вполне юзаемо, хотя это только начало. Как по мне — неплохо.
                    Хотелось бы узнать, что такого революционного должно появиться в Django, чтобы автор (или согласные с ним комментирующие) решил, что это — достойное развитие? Только без фраз типа «сделайте нормальную ORM». А то если дойдем до вкусовщины и каждый найдет свой любимый рецепт для решения той или иной задачи.
                      0
                      что меня удивит — лежит в зоне правки нескольких объектов:
                      Возможность инлайнов в середине фиелдсета формы админпанели.
                      (Правится переопределением change_form)
                      Возможность переменных фиелдсетов в формсетах (Правится переопределением __iter__ филдсета)
                      Асинхронные формсеты (в текущей реализации невозможно)
                      Очень удивит, если появится документация по объекту query
                      Отказ от жесткой привязки к jquery тоже порадует.
                      То что менеджер шаблонов сделали асинхронным… как то все равно. Или там еще что-то изменилось?
                        0
                        Хз, как по мне, так из перечисленного Вами… Ну ладно, что-то дейсствительно масштабно (про формсеты). В остальном — не то чтобы незначительные изменения, но ничего такого, что звучало бы хотя бы близко к пресловутым async во вьюхах =D.
                        Я не говорю, что предложенные Вами изменения несущественны. Но посудите сами: неужели инлайны в админке — то, чего ждет все комьюнити? Сделают, может. Будет этакий междудельный фикс минорный. Или сами пулл-реквестик протолкните. Тогда, если когда-нибудь увидимся, — с меня пиво =D.

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

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