Как решить проблему синглтонов в Django ModelAdmin.

Класс ModelAdmin из django.contrib.admin.options имеет плохой дизайн с самого начала: каждый зарегистрированный ModelAdmin в вашем проекте является синглтоном.

Этот факт создает большой барьер для каждого разработчика, который использует Django Admin в своем проекте. Это замедляет работу проекта, создает проблемы, когда один и тот же ModelAdmin используется несколькими пользователями, и побуждает программистов использовать хаки, чтобы обойти ограничения этого (анти) паттерна. Как решить эту проблему за 5 строк, вы можете узнать в этой статье.

Почему разработчики django.contrib.admin продолжают полагаться на эту архитектуру — для меня загадка.

К счастью, для этой проблемы было найдено быстрое и простое исправление, которое работает от первой версии Django до текущей (4.0.3 в апреле 2022 года).

Оглавление:

  • Проблемы. Я подробно опишу проблемы, возникающие при использовании архитектуры singleton для ModelAdmin.
  • Решение. Я проведу вас через исправление и объясню, как и почему оно работает.
  • Разработчики Django. Я прошу разработчиков Django, читающих эту статью, рассмотреть возможность изменения структуры django.contrib.admin, чтобы сделать жизнь проще для всех.
  • Выступление на PyCon 2022 об этой проблеме. В дополнение к этой статье у меня есть видео на эту тему с PyCon DE 2022 в Берлине.

Проблемы

1. Несколько пользователей не могут нормально работать с одним и тем же ModelAdmin одновременно.

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

Позвольте мне продемонстрировать это на примере кода из моей предыдущей статьи о динамическом инлайн-позиционировании в админке Django. Сейчас не так важно, что делает код. Мы просто изменяем функции в панели администратора вот таким образом:

Теперь давайте откроем две вкладки в панели администратора, чтобы имитировать двух пользователей. Теперь откроем форму изменения для продукта с pk=1 на первой вкладке (/admin/products/product/1/change). После этой строки нас остановит breakpoint:

10            self.hello = 'hello this is the first obj'
Войти в полноэкранный режим Выход из полноэкранного режима

Если мы проверим атрибут self.hello, то увидим следующее:

Теперь давайте откроем форму изменения для продукта с pk=2 в другой вкладке (‘/admin/products/product/2/change’) и посмотрим, что произойдет. Мы сразу получаем ответ от формы изменения, потому что мы пропустили breakpoint. Но давайте посмотрим, что произошло с нашим атрибутом hello, который мы установили для нашего ModelAdmin в первой вкладке.

Наш атрибут ModelAdmin.hello для GET запроса pk=1 был изменен на «это второй obj», хотя мы ничего не делали в нашем первом потоке.

Мы доказали, что ModelAdmin является синглтоном, что означает, что мы всегда получаем один и тот же экземпляр ModelAdmin.

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

При таком поведении каждый ModelAdmin’s не гарантирует целостность данных, потому что невозможно отследить, кто редактировал данные. Редактировал ли этот текущий поток наши данные? Изменил ли другой запрос что-то без нашего ведома? Кто знает.

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

Это вызывает ошибки, для которых нет очевидного объяснения без глубокого понимания архитектуры панели администратора.

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

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

Если мы не можем использовать ModelAdmin, то где мы можем хранить наши данные? Django по умолчанию не дает нам четкого ответа.

2. Хаки

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

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

3. Скорость

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

В таблице показано, как часто следующие методы ModelAdmin вызываются несколько раз за запрос:

список изменений GET изменение GET изменение ПОСТ добавить GET добавить ПОСТ удалить
has_view_permission 5 3 4 3
has_module_permission 3 3 3
get_ordering 2 3 3 4 3
get_preserved_filters 2 2 2 2
has_change_permission 9 4 4 4
get_list_display 2
get_search_fields 2
get_model_perms 3 3 3
has_delete_permission 4 3 4 3 3
get_readonly_fields 2 2
get_search_results
has_add_permission 4 3 6 3
get_empty_value_display переменная переменная переменная
get_actions 3
_get_base_actions

Количество вызовов некоторых методов зависит от количества полей AdminForm или ModelForm.

Давайте сравним время отклика «ванильной» админ-панели Django по умолчанию и админ-панели Django с кэшированным методом ModelAdmin. В качестве примера кода я взял проект из моей прошлой статьи:

Я создал промежуточное ПО, которое выводит время, прошедшее между запросом и ответом:

Если код middleware будет выполнен, мы получим результат:

стандартная админ-панель django модифицированная админ-панель
среднее время: 0.031673 с среднее время: 0.026001с

Как видите, даже в этом простом проекте прирост скорости за счет кэширования результатов вычисления функций составляет **почти 20%!

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

Решение

Давайте посмотрим, откуда в Django берутся синглтоны ModelAdmin и попытаемся это исправить.

Каждый экземпляр ModelAdmin создается при регистрации экземпляра AdminSite из django.contrib.admin.sites.

В docstring класса AdminSite мы узнаем, что метод get_urls используется для получения представлений из каждого зарегистрированного экземпляра ModelAdmin. Спасибо автору этой docstring.

В AdminSite.get_urls мы можем найти этот фрагмент кода:

Здесь мы видим, что вызывается свойство ModelAdmin.urls.
Это свойство возвращает результат метода ModelAdmin.get_urls. Итак, давайте рассмотрим, что делает этот метод get_urls:

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

Возвращаемый список создается для url-dispatcher. Он содержит кортежи из url, представления, которое нужно вызвать, и имени представления.

Для нас важно то, что дается в качестве представления. Это обернутый ограниченный метод экземпляра ModelAdmin. Итак, давайте посмотрим, что делает эта обертка wrap.

К счастью, эта обертка wrap определена прямо выше.
update_wrapper в конце wrap не очень важен, он просто заставляет конечное представление вести себя как экземпляр ModelAdmin.

Тело функции wrapper имеет решающее значение для нашей задачи. Там мы видим вызов admin_site.admin_view с ограниченным методом экземпляра ModelAdmin в качестве аргумента.

Этот метод нам нужно переопределить, чтобы избавить Django ModelAdmin от проклятия шаблона singleton.

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

Давайте создадим дочерний класс AdminSite и переопределим метод admin_view следующим образом:

Давайте проигнорируем все обертки и сосредоточимся на этих строках кода:

new_instance = type(instance)(instance.model, instance.admin_site)
return func.__func__(new_instance, *args, **kwargs)
Вход в полноэкранный режим Выйти из полноэкранного режима

func — это ограниченный метод нашего синглтонного экземпляра ModelAdmin, который всегда один и тот же для каждого запроса.

Атрибут __func__ функции func является методом класса ModelAdmin. Он не связан больше ни с одним экземпляром ModelAdmin.

Здесь я использую питоновскую терминологию связанного и несвязанного метода. func.__func__ не связан с экземпляром func.__self__, но это также не статическая функция, а метод класса. Экземпляр того же класса, что и func.__self__, по-прежнему требуется в качестве первого аргумента для вызова этого метода.

Для создания нового экземпляра ModelAdmin в качестве аргументов нам нужны model и admin_site, которые можно легко взять из старого экземпляра singleton.

Теперь мы можем вернуть вызов ‘func’ с новым экземпляром в качестве self и аргументами *args и **kwargs, которые передаются во время вызова.

Теперь архитектура синглтонов успешно обойдена.

Это означает, что каждый раз, когда запрос посылается представлению, создается новый экземпляр ModelAdmin и вызывается соответствующая функция представления.

Как уже упоминалось ранее, можно было избежать всех этих мучений с самого начала работы с Django.

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

Мы можем создать дочерний класс AdminConfig следующим образом:

Атрибут default_site должен соответствовать вашему классу AdminSite.

Наконец, зарегистрируйте AdminConfig в settings.py в INSTALLED_APPS, чтобы завершить изменения. Не забудьте удалить панель администратора по умолчанию.

Новая админ-панель в ее нынешнем виде не быстрее старой. Нам по-прежнему необходимо кэшировать методы ModelAdmin. Для этого можно создать Mixin для ModelAdmin и обернуть вышеуказанные методы следующим образом:

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

Заключение

Мне нравится Django Framework. Это мощный инструмент, который помогает мне создавать великие вещи. Я могу решить любую проблему этого фреймворка с помощью небольшого кусочка кода в нужном месте. Но найти это место не всегда легко. И документация Django не может помочь. Отсутствие документации — это самая большая проблема Django.

Разработчики Django

Я надеюсь, что после тех пунктов, которые я изложил в этой статье, вы согласитесь, что существуют серьезные проблемы с реализацией в Django архитектуры singleton в панели администратора.
Этот паттерн делает работу с админ-панелью громоздкой и медленной, и нужно что-то делать, чтобы это исправить. И я пишу здесь, как это можно сделать.

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

Доклад о проблеме синглтонов на PyCon 2022

Я говорил об этой проблеме на многих мероприятиях. Вы можете посмотреть мое видео с PyCon DE в Берлине в апреле 2022 года, я писал здесь, что буду выступать на этой конференции.

Оцените статью
Procodings.ru
Добавить комментарий