Пакетне видалення в Django REST Framework (DRF) за допомогою ModelViewSet

текст перекладу
Одна з чудових особливостей DRF (Django REST Framework) — це Viewset. Вона дозволяє розробнику визначати набір вбудованих обробників дій для ресурсу. Після реєстрації з використанням стандартного маршрутизатора DRF, вона може надати стандартний набір операцій CRUD для ресурсу.

Ця стаття пояснює, як додати підтримку для користувацької дії під назвою bulk-destroy, яка дозволяє виконати видалення кількох об'єктів одночасно за допомогою демонстраційного додатку.

Досвід роботи з django та DRF буде корисний для кращого розуміння статті.

Завдання

В одному з наших проєктів ми активно використовували viewsets (ModelViewSet) в нашому Django додатку для виконання простих операцій CRUD з ресурсами. Було кілька ресурсів, де автентифіковані та авторизовані користувачі повинні були виконувати операцію видалення кількох записів/об'єктів одночасно. Якщо б це стосувалося одного ресурсу, ми могли б використовувати спеціальну реалізацію виду з класом APIView або інший спосіб. Однак додаток зростав, і нам потрібно було знайти загальний підхід для вирішення цього завдання. Зрештою, ми вирішили створити дію bulk-destroy, використовуючи маршрут для списку, щоб її можна було викликати за допомогою HTTP методу DELETE, подібно до того, як метод list викликається за допомогою методу GET. Звісно, щоб виконати bulk-destroy, потрібно передати ідентифікатори об'єктів, які потрібно видалити, щоб додаток міг перевірити права користувача та успішно видалити запитувані об'єкти.

pic

Фото від Yoko Correia Nishimiya на Unsplash

Демонстраційний додаток

Ми розглянемо демонстраційний додаток, який використовує дію bulk-destroy. Повний код додатку доступний тут.

В цьому демонстраційному додатку я створив простий ресурс Note, для якого Django модель виглядає наступним чином:

from django.conf import settings  
from django.db import models  

class Note(models.Model):  
 title = models.CharField(max_length=1024, blank=True, null=True)  
 body = models.TextField(blank=True, null=True)  
 created_at = models.DateTimeField(auto_now_add=True, editable=False)  
 modified_at = models.DateTimeField(auto_now=True, editable=False)  
 owner = models.ForeignKey(settings.AUTH_USER_MODEL,  
 null=True, editable=False, db_index=True,  
 on_delete=models.SET_NULL, related_name="note_owner")  

 class Meta:  
 db_table = "note"  

 def __str__(self):  
 return f"(pk: {self.pk}, title: {self.title})"

Ідея полягає в тому, що користувач може володіти кількома нотатками з заданими title та body. Модель користувача є стандартною моделлю Django auth-user.

Тепер, щоб підтримати дію bulk-destroy у вигляді HTTP REST API з методом DELETE, нам потрібно оновити налаштування маршрутизатора в DRF, який надає URL-адреси клієнту.

# router.py  
from rest_framework.routers import DefaultRouter  

from restapis import views  

class CustomAPIRouter(DefaultRouter):  
 def __init__(self, *args, **kwargs):  
 super().__init__(*args, **kwargs)  

 # за визначенням DRF, це може змінюватися залежно від версії DRF  
 list_route = self.routes[0]  
 list_route.mapping.update({  
 "delete": "bulk_destroy"  
 })  

# Маршрутизатори забезпечують легкий спосіб автоматично визначати конфігурацію URL  
APIRouter = CustomAPIRouter()  

# маршрутизатори для нотаток API  
APIRouter.register(r'notes', views.NoteViewSet, basename="notes")

Як видно з наведеного фрагмента коду, ми перевизначили маршрутизатор, успадкувавши клас DefaultRouter від DRF. Ми витягли маршрут для списку та оновили його маппінг, додавши HTTP метод delete до методу bulk_destroy виду. Іншими словами, тепер viewset може реалізувати метод bulk_destroy для обробки запиту на видалення кількох об'єктів для цього ресурсу.

Клас DefaultRouter успадковує клас SimpleRouter.
текст перекладу
Подивіться на код класу SimpleRouter тут, щоб зрозуміти, чому ми взяли перший елемент з маршрутів, щоб вважати його маршрутом для списку. Більше деталей про кастомні маршрути тут.

Для підтримки загальної поведінки дії bulk destroy в viewset на основі моделі, ми можемо ввести міксін під назвою BulkDestroyModelMixin. Насправді, ModelViewSet — це просто група міксінів для підтримки базових операцій CRUD для ресурсу. Реалізацію можна побачити тут.

# serializers  

from rest_framework import serializers  

class BulkDestroyInstanceSerializer(serializers.Serializer):  
 """  
 ids усіх об'єктів, що мають бути знищені  
 """  
 ids = serializers.ListField(  
 child=serializers.IntegerField(min_value=1),  
 write_only=True  
 )
# mixins.py  

import logging  

from rest_framework import status  
from rest_framework.response import Response  
from rest_framework.exceptions import ValidationError, PermissionDenied  

from ..serializers import BulkDestroyInstanceSerializer  


log = logging.getLogger(__name__)  


class BulkDestroyModelMixin:  
 """  
 BulkDestroyModelMixin надає гнучкість для видалення  
 кількох ресурсів через кінцеву точку списку за допомогою методу delete.  
 """  

 # на випадок, якщо у viewset не визначена кількість для bulk-destroy  
 __DEFAULT_BULK_DESTROY_COUNT = 5  

 # максимальна кількість для безпечного видалення  
 __MAX_BULK_DESTROY_COUNT = 10  

 def bulk_destroy(self, request, *args, **kwargs):  
 # якщо не дозволено, то повернути помилку  
 if not getattr(self, "allow_bulk_destroy_method", False):  
 return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)  

 serializer = BulkDestroyInstanceSerializer(data=request.data)  
 serializer.is_valid(raise_exception=True)  

 # отримуємо ids з перевірених даних серіалізатора  
 destroy_instance_id_list = serializer.validated_data.get("ids", [])  

 # перевірка: хоча б один id має бути наданий  
 if len(destroy_instance_id_list) == 0:  
 raise ValidationError("необхідно вказати хоча б один id об'єкта для виконання операції пакетного видалення")  

 # максимальна дозволена кількість для bulk destroy  
 bulk_destroy_count = getattr(self, "bulk_destroy_count", 0) or self.__DEFAULT_BULK_DESTROY_COUNT  
 bulk_destroy_count = min(bulk_destroy_count, self.__MAX_BULK_DESTROY_COUNT)  
 if len(destroy_instance_id_list) > bulk_destroy_count:  
 raise PermissionDenied(f"Ви не маєте дозволу видаляти більше ніж {bulk_destroy_count} об'єктів")  

 # можуть бути валідні id, до яких запитуваний користувач не має доступу  
 # тому повертаємо помилку доступу  
 valid_bulk_instance_qs = self.get_queryset().filter(pk__in=destroy_instance_id_list)  
 if len(destroy_instance_id_list) != valid_bulk_instance_qs.count():  
 raise PermissionDenied(detail="Ви не маєте дозволу видаляти один або кілька об'єктів")  

 for permission in self.get_permissions():  
 if (  
 hasattr(permission, "has_bulk_destroy_permission") and  
 not permission.has_bulk_destroy_permission(request, self, valid_bulk_instance_qs)  
 ):  
 raise PermissionDenied(detail="Ви не маєте дозволу видаляти один або кілька об'єктів")  


 # після всіх перевірок виконуємо пакетне видалення  
 self.perform_bulk_destroy(valid_bulk_instance_qs)  

 # повертаємо відповідь  
 return Response(status=status.HTTP_204_NO_CONTENT)  

 def perform_bulk_destroy(self, bulk_instance_qs):  
 log.info(f"bulk destroy, instance list: {list(bulk_instance_qs)}")  
 # використовуємо відфільтрований queryset для видалення  
 bulk_instance_qs.delete()

Цей міксін реалізує метод виду для дії bulk_destroy, яку ми оновили в попередньому кастомному маршруті.
текст перекладу
Простими словами, цей міксін приймає ids, список ідентифікаторів об'єктів, що мають бути видалені, проводить перевірку на валідність, перевірку прав доступу і, якщо дозволено, видаляє запитувані об'єкти.

Тепер нам потрібно успадкувати цей міксін BulkDestroyModelMixin в кінцевому viewset. У нашому прикладі ресурсу Note це можна використати так:

# views.py  

from rest_framework import viewsets  
from rest_framework.permissions import IsAuthenticated  

from ..models import Note  
from ..serializers import NoteSerializer  
from ..permissions import NotePermission  
from .mixins import BulkDestroyModelMixin  


class NoteViewSet(BulkDestroyModelMixin, viewsets.ModelViewSet):  
 """  
 NoteViewSet для обробки CRUD операцій з bulk-destroy.  
 """  
 queryset = Note.objects.all()  
 serializer_class = NoteSerializer  
 permission_classes = (IsAuthenticated, NotePermission, )  
 allow_bulk_destroy_method = True  
 bulk_destroy_count = 5  

 def get_queryset(self):  
 return Note.objects.filter(owner=self.request.user)

Зверніть увагу, що міксін також підтримує наступні атрибути, які можна визначити у viewset, якщо це потрібно:

  • allow_bulk_destroy_method: якщо viewset дозволяє bulk destroy
  • bulk_destroy_count: кількість об'єктів, які можна видалити за один запит

Також, якщо з якихось причин потрібна додаткова кастомна перевірка доступу для дії пакетного видалення, можна визначити метод has_bulk_destroy_permission у класі дозволів (permissions) для viewset. Більше деталей про дозволи тут!

Нижче — попередньо записаний відео-демо, яке демонструє дію bulk-destroy для ресурсу note.

pic

Щоб зареєструвати користувачів у django shell:

root@17548402114a:/usr/src/app/mysite# python manage.py shell  
Python 3.12.7 (main, Oct 1 2024, 22:28:30) [GCC 12.2.0] on linux  
Type "help", "copyright", "credits" or "license" for more information.  
(InteractiveConsole)  
>>> from django.contrib.auth.models import User  
>>> r = User.objects.create(username="[email protected]", email="[email protected]", first_name="Ramesh")  
>>> r.set_password("r#2025")  
>>> s = User.objects.create(username="[email protected]", email="[email protected]", first_name="Suresh")  
>>> s.set_password("s#2025")

Щоб згенерувати токен авторизації для користувачів:

root@17548402114a:/usr/src/app/mysite# python manage.py drf_create_token -r [email protected]  
Generated token  for user [email protected]  
root@17548402114a:/usr/src/app/mysite# python manage.py drf_create_token -r [email protected]  
Generated token  for user [email protected]

Висновок

Цей підхід до обробки пакетного видалення був дуже корисним у одному з минулих проектів, де додаток django+DRF активно використовував viewsets на основі моделей. Сподіваюся, що це надихне вас на проектування та реалізацію будь-якої кастомної дії в DRF.

Дякую за те, що прочитали статтю!

Перекладено з: Bulk destroy in Django REST Framework (DRF) using ModelViewSet

Leave a Reply

Your email address will not be published. Required fields are marked *