You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Django REST Framework视图集中通过Query处理DELETE、PUT请求?

如何通过Query参数处理Django REST Framework的DELETE、PUT请求?

问题背景

作为Django新手,我已为Web应用实现Token认证,项目(hotels_app)中的核心代码如下:

models.py

class Hotel(UUIDMixin):
    company = models.CharField(_('company'), max_length=config.CHARS_DEFAULT, blank=True, null=True)
    name = models.CharField(_('name'), max_length=config.CHARS_DEFAULT)
    star_rating = models.PositiveSmallIntegerField(
        verbose_name=_('star rating'),
        blank=True,
        null=True,
        validators=[MaxValueValidator(5)],
    )
    rooms_quantity = models.PositiveIntegerField(_('rooms quantity'))
    phone = PhoneNumberField(
        verbose_name=_('phone'),
        max_length=config.PHONE_LENGTH,
        blank=True,
        null=True,
    )
    check_in = models.CharField(_('check in'), max_length=config.CHECK_IN_OUT_LEN)
    check_out = models.CharField(_('check out'), max_length=config.CHECK_IN_OUT_LEN)
    description = models.TextField(_('description'), blank=True, null=True)
    country = models.CharField(_('country'), max_length=config.CHARS_DEFAULT)
    state = models.CharField(_('state'), max_length=config.CHARS_DEFAULT, blank=True, null=True)
    city = models.CharField(_('city'), max_length=config.CHARS_DEFAULT)
    street = models.CharField(_('street'), max_length=config.CHARS_DEFAULT)
    building = models.CharField(_('building'), max_length=config.CHAR_BUILD_LENGTH)
    latitude = models.FloatField(
        verbose_name=_('latitude'),
        validators=[MinValueValidator(-config.LATITUDE), MaxValueValidator(config.LATITUDE)],
    )
    longitude = models.FloatField(
        verbose_name=_('longitude'),
        validators=[MinValueValidator(-config.LONGITUDE), MaxValueValidator(config.LONGITUDE)],
    )
    amenities = models.ManyToManyField(Amenity, verbose_name=_('amenities'), through='HotelAmenity')

    def __str__(self):
        return f'{self.name}, {self.country}'

    class Meta:
        indexes = [models.Index(fields=['name'], name='hotel_name_idx')]
        constraints = [
            models.CheckConstraint(
                check=models.Q(star_rating__gte=0) & models.Q(star_rating__lte=5),
                name='star_rate_check',
            ),
            models.CheckConstraint(check=models.Q(rooms_quantity__gt=0), name='rooms_quan_check'),
            models.CheckConstraint(
                check=models.Q(latitude__gte=-config.LATITUDE) & models.Q(latitude__lte=config.LATITUDE),
                name='latitude_check',
            ),
            models.CheckConstraint(
                check=models.Q(longitude__gte=-config.LONGITUDE) & models.Q(longitude__lte=config.LONGITUDE),
                name='longitude_check',
            ),
        ]
        ordering = ['name']
        db_table = 'hotel'
        verbose_name = _('hotel')
        verbose_name_plural = _('hotels')

serializers.py

class HotelSerializer(HyperlinkedModelSerializer):

    class Meta:
        model = Hotel
        fields = (
            'id', 'company', 'name',
            'star_rating', 'rooms_quantity', 'phone',
            'check_in', 'check_out', 'description',
            'country', 'state', 'city', 'street',
            'building', 'latitude', 'longitude',
        )

views.py

class Permission(permissions.BasePermission):
    def has_permission(self, request, _):
        if request.method in config.SAFE_METHODS:
            return bool(request.user and request.user.is_authenticated)
        elif request.method in config.UNSAFE_METHODS:
            return bool(request.user and request.user.is_superuser)
        return False


def query_from_request(request, serializer=None) -> dict:
    if serializer:
        query = {}
        for attr in serializer.Meta.fields:
            attr_value = request.GET.get(attr, '')
            if attr_value:
                query[attr] = attr_value
        return query
    return request.GET

def create_viewset(model, serializer):
    class CustomViewSet(viewsets.ModelViewSet):
        serializer_class = serializer
        permission_classes = [Permission]

        def get_queryset(self):
            instances = model.objects.all()
            query = query_from_request(self.request, serializer)
            if query:
                instances = instances.filter(**query)
            return instances

    return CustomViewSet

HotelViewSet = create_viewset(Hotel, serializers.HotelSerializer)

urls.py

router = DefaultRouter()
router.register(r'amenity', views.AmenityViewSet, basename='amenity')
router.register(r'hotel', views.HotelViewSet, basename='hotel')
router.register(r'room', views.RoomViewSet, basename='room')

urlpatterns = [
    # REST
    path('rest/', include(router.urls)),
]

问题描述

GET请求带Query参数可正常过滤数据(如http://127.0.0.1:8000/rest/hotel/?company=some_company),但向同一URL发送DELETE或PUT请求时,服务器返回:

{
    "detail": "Method \"DELETE\" not allowed."
}

已携带Token认证头,尝试过添加MultipleFieldLookupMixin、重写destroy和update方法,均无效。

原因分析

Django REST Framework的ModelViewSet通过DefaultRouter注册后,列表路由(如/rest/hotel/)默认仅支持GET(list)、POST(create)方法,DELETE和PUT方法被映射到详情路由(如/rest/hotel/<pk>/),用于操作单个实例。直接向列表URL发送DELETE/PUT会触发方法不允许的错误。

解决方案

要实现通过Query参数批量删除/更新实例,推荐使用DRF的自定义动作(Action),在ViewSet中添加批量操作的接口,符合REST规范且无需修改路由配置。

修改views.py中的ViewSet定义

在create_viewset函数的CustomViewSet类中添加以下自定义动作:

from rest_framework.decorators import action
from rest_framework import status
from rest_framework.response import Response

def create_viewset(model, serializer):
    class CustomViewSet(viewsets.ModelViewSet):
        serializer_class = serializer
        permission_classes = [Permission]

        def get_queryset(self):
            instances = model.objects.all()
            query = query_from_request(self.request, serializer)
            if query:
                instances = instances.filter(**query)
            return instances

        # 批量删除动作
        @action(detail=False, methods=['delete'], url_path='batch-delete')
        def batch_delete(self, request):
            query = query_from_request(request, self.serializer_class)
            if not query:
                return Response('缺少查询参数', status=status.HTTP_400_BAD_REQUEST)
            
            instances = self.get_queryset().filter(**query)
            count = instances.count()
            if count == 0:
                return Response(f'未找到匹配{model.__name__}的实例', status=status.HTTP_404_NOT_FOUND)
            
            try:
                instances.delete()
            except Exception as e:
                return Response(str(e), status=status.HTTP_500_INTERNAL_SERVER_ERROR)
            
            return (
                Response(f'成功删除{count}个{model.__name__}实例', status=status.HTTP_200_OK)
                if count > 1 else
                Response(status=status.HTTP_204_NO_CONTENT)
            )

        # 批量更新动作
        @action(detail=False, methods=['put'], url_path='batch-update')
        def batch_update(self, request):
            query = query_from_request(request, self.serializer_class)
            if not query:
                return Response('缺少查询参数', status=status.HTTP_400_BAD_REQUEST)
            
            update_data = request.data
            if not update_data:
                return Response('缺少更新数据', status=status.HTTP_400_BAD_REQUEST)
            
            instances = self.get_queryset().filter(**query)
            count = instances.count()
            if count == 0:
                return Response(f'未找到匹配{model.__name__}的实例', status=status.HTTP_404_NOT_FOUND)
            
            try:
                # 批量更新字段(仅支持可直接更新的字段,复杂关联字段需单独处理)
                instances.update(**update_data)
                # 返回更新后的实例数据
                updated_instances = self.get_queryset().filter(**query)
                serializer = self.get_serializer(updated_instances, many=True)
                return Response(serializer.data, status=status.HTTP_200_OK)
            except Exception as e:
                return Response(str(e), status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    return CustomViewSet

使用方法

  1. 批量删除:向http://127.0.0.1:8000/rest/hotel/batch-delete/?company=some_company发送DELETE请求
  2. 批量更新:向http://127.0.0.1:8000/rest/hotel/batch-update/?company=some_company发送PUT请求,请求体携带要更新的字段(如{"star_rating": 4})

注意事项

  • 批量更新仅支持Django ORM的update()方法能处理的字段,涉及多对多、外键等关联字段的更新需额外逻辑处理
  • 确保请求用户具备超级用户权限(已通过自定义Permission类控制)
  • 若需要更复杂的过滤逻辑,可在query_from_request函数中补充字段的查询类型(如模糊匹配、范围查询等)

内容的提问来源于stack exchange,提问作者Anna

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 06:27:00