Использование ListSerializer с bulk_update для создания эффективных конечных точек PUT API с Django Rest Framework

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

В части 1: Эффективное массовое создание с помощью Django Rest Framework » мы рассмотрели, как оптимизировать POST API с помощью Django Rest Framework. В этой статье я покажу, что вам нужно повысить производительность вызовов API PUT с помощью ListSerializers с методом bulk_update. Полноценное рабочее приложение Django с кодом и модульными тестами можно найти на GitHub здесь.

Цели

К концу этого урока вы сможете

  1. Внедрите PUT API для обновления моделей баз данных с помощью стандартного рабочего процесса Django Rest Framework.
  2. Измените этот API, чтобы выполнять пакетные обновления с помощью ListSerializer.
  3. Профилируйте и оптимизируйте ListSerializer для достижения 10-кратного повышения производительности за счет использования bulk_update и уменьшения количества обращений к базе данных.

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

Обзор Django API

Вы можете почувствовать, что высокий уровень абстракции Django Rest Framework затрудняет понимание того, что происходит. Я тоже обнаружил, что вначале у них была очень крутая кривая обучения, но как только вы познакомитесь со структурой, их использование для управления большим проектом с множеством API-интерфейсов может привести к значительному увеличению производительности. Вы также обнаружите, что сотрудничать становится проще, если все в вашей команде следуют одним и тем же образцам. Как правило, создание Django Rest Framework API состоит из 5 основных частей. Вот они:

  1. Модели, которые управляют отношениями между таблицей базы данных и Python.
  2. Сериализаторы, которые проверяют и сериализуют входящие и исходящие данные.
  3. Наборы запросов, которые создают, запрашивают и сохраняют результаты запросов к базе данных в виде экземпляров модели.
  4. Представления, которые представляют собой классы, объединяющие модель, сериализатор, набор запросов для каждой конечной точки.
  5. URL-адреса, указывающие, когда следует вызывать Просмотр.

Теперь мы рассмотрим их код, чтобы создать наш REST API.

Модель

Django ORM использует модели для управления взаимодействием с серверной базой данных. В этом руководстве мы создаем две модели: модель проекта, с которой связаны модели задач. Модели определены в models.py как:

Кроме того, всякий раз, когда обновляется новая задача, бизнес-логика требует, чтобы последний измененный проект был обновлен. Для обработки бизнес-логики мы используем API сигналов с post_save и post_delete для задачи. Таким образом, после вызова сохранения будет также активирован сигнал обновления проекта last_modified.

Просмотры

Общие классы представлений - это абстрактные методы из Django Rest Framework, которые используются для реализации HTML-методов POST / PUT / GET / DELETE. Для конечной точки обновления мы будем использовать общий UpdateAPIView, который предоставляет обработчик метода PUT. Чтобы лучше понять базовый поток управления в представлении обновления Django, я собрал следующую блок-схему для использования в качестве справки.

Для PUT API мы используем UpdateAPIView для выполнения обновлений модели Task. Для этого нам нужно определить сериализатор, метод get_queryset и URL API, вызывающего функцию.

Если вы не знакомы с Django Rest Framework, то, что вы можете заметить в нашем коде TaskUpdateView выше, является то, что кода там не так много. Для базового API почти все необходимое для выполнения этой работы полностью абстрагировано. Чтобы оптимизировать этот API, нам придется переопределить многие из унаследованных методов. Для получения более подробной документации по представлениям ознакомьтесь с официальным руководством здесь.

QuerySet

queryset - это часть Django ORM, которая упрощает взаимодействие между вашими моделями и базой данных. Наборы запросов - это мощные абстракции, которые позволяют программно создавать сложные запросы к базе данных. В нашей функции get_queryset мы используем фильтр для объекта Task, чтобы возвращать только те задачи, у которых есть указанный пользователем проект в качестве внешнего ключа и идентификатора задачи. Набор запросов содержит список экземпляров, которые мы можем обновить, а затем сохранить обратно в базу данных с данными, переданными пользователем.

Сериализаторы

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

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

Поскольку id проекта указан в URL-адресе, мы используем поле Hidden с классом CurrentProjectDefault, чтобы указать, как извлечь идентификатор проекта из запроса и получить проект. объект из базы данных. Класс CurrentProjectDefault определяется следующим образом.

Со всеми этими элементами мы завершили базовую реализацию нашего API. Давайте продолжим и расскажем о производительности. Для этого мы будем использовать pytest для создания модульного теста, в котором мы обновляем 10 000 объектов Task, вызывая API один раз для каждого обновления.

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

>> py.test --durations=1
    
==================
101.21s call test_update_task

Как видите, он такой медленный! Выполнение 10 000 обновлений занимает около 100 секунд.

ListSerializer

Давайте посмотрим, как повысить производительность кода. Первая оптимизация, которую мы сделаем, - это переключимся на использование ListSerializer. Сериализатор списка позволит вам отправить один запрос на несколько обновлений. Сначала мы создадим класс UpdateListSerializer, который расширяет ListSerializer.

Вычисляя instance_hash, нам не нужно индексировать экземпляр, что очень неэффективно. Затем мы изменим свойства Meta нашего TaskSerializer, чтобы использовать новый класс сериализатора списков.

Нам также нужно будет изменить наш URL-адрес, теперь он будет принимать только project_id как часть пути, а task_id будет включен как часть объекта данных, отправленного в PUT API.

Наконец, мы создадим новое представление; TaskUpdateListView. Здесь мы перезапишем базовый метод get_serializer, чтобы проверить входные данные, которые являются списком. Когда мы обнаруживаем ввод, в который пользователь передал список, мы устанавливаем свойство kwargs [«many»] = True. Это сообщает сериализатору, что он должен использовать list_serializer_class перед вызовом отдельных обновлений для каждой Задачи.

Мы также перезаписываем базовый метод обновления представления. Теперь он выполнит простую проверку ids перед вызовом модифицированного метода get_queryset, который принимает ids в качестве входных данных. Набор запросов вернет экземпляры для всех объектов Task, которые пользователь запросил обновить.

Со всеми этими оптимизациями мы теперь используем ListSerializer для массового обновления моделей задач. Мы создадим еще один модульный тест, чтобы профилировать производительность.

Давайте продолжим и проверим эффективность этого изменения.

>> py.test --durations=2
    
==================
101.21s call test_update_task
55.98s call test_update_list_serializer

Итак, просто добавив метод ListSerializer, мы можем увидеть это двукратное улучшение производительности. Тем не менее, 55 секунд на 10 000 обновлений - это медленно. Ключом к дальнейшей оптимизации производительности этого API будет сокращение количества обращений к базе данных.

Консолидировать логику

В настоящее время наш сериализатор вызывает CurrentProjectDefault, чтобы получить проект, связанный с каждым создаваемым им объектом экземпляра Task. Вместо этого мы собираемся изменить функцию put нашего представления, чтобы вытащить проект и вставить его в объект request.data. Таким образом, нам нужно сделать только одно обращение к базе данных, чтобы получить проект для всех наших Задач.

Нам также нужно будет заменить поле CurrentProjectDefault в нашем сериализаторе настраиваемым полем. Мы создаем настраиваемое поле с именем ModelObjectidField, которое возвращает только переданные в него данные.

bulk_update

Затем мы создадим BulkUpdateListSerializer, который будет использовать bulk_update Django, представленный в Django 2.2. Эта функция позволяет выполнять массовые обновления в базе данных, передавая список экземпляров для обновления. Следующий код описывает BulkUpdateListSerializer.

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

to_presentation

Функция to_presentation является частью сериализатора, который управляет преобразованием экземпляров в сериализованные объекты, которые могут быть возвращены пользователю. Метод to_presentation по умолчанию для ListSerializer очень неэффективен при получении значения instance.project id для возврата. В следующем коде to_presentation мы используем тот факт, что все идентификаторы проекта одинаковы, поэтому нам нужно получить это свойство только один раз.

А как насчет наших сигналов?

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

Следует отметить, что функция bulk_update не изменяет поля auto_add_now в базе данных. Чтобы преодолеть это, мы явно устанавливаем поле last_modified для всех наших экземпляров, чтобы обновление выполняло их.

Наконец, давайте проверим производительность нашей новой функции с помощью bulk_update.

И вот результаты:

>> py.test --durations=3
    
==================
101.21s call test_update_task
55.98s call test_update_list_serializer
12.06s call test_bulk_update_list_serializer

Как видите, теперь тест выполняется примерно за 20 секунд. Это примерно 10-кратное улучшение скорости, которое не требует слишком большой дополнительной сложности кода.

Резюме

При этом мы рассмотрели, как можно повысить производительность вашего приложения Django с помощью ListSerializer и функции bulk_update, представленных в Django 2.2. Эти два метода вместе с уделением особого внимания минимизации количества вызовов, необходимых для вашей базы данных, могут дать вам более 10-кратное повышение производительности без особых дополнительных усилий.

Опять же, полностью рабочий проект Django со всем этим кодом и модульными тестами можно найти на GitHub в репозитории d ango_bulk_tutorial и в предыдущем посте Часть 1: Эффективное массовое создание с Django Rest Framework . Надеюсь, вам понравился этот пост, и обязательно подпишитесь на меня, чтобы увидеть больше статей о Django, Python, DevOps, машинном обучении и Tinyml. У вас есть уловки для оптимизации Django, которые вам нравятся? Оставьте комментарий или оставьте отзыв о статье.