Django 架设 Restful API(五)API开发:资源API实现

所有步骤中的账号密码仅供参考,千万不要在自己的生产环境中使用,否则产生的安全问题由您自己承担。

1.Restful API 实现

  1. API使用专用域名:部署时添加子域名解析。
  2. API的版本号放入URL中django rest framework API版本控制器实现。
  3. URL中不能有动词只能有名词,而且所用的名词往往与数据库的表格名对应。
  4. HTTP动词与资源操作相对应:3、4使用django rest framework自动路由器实现。
  5. 过滤返回的信息:django rest framework过滤器。
  6. 状态码和错误处理:django rest framework Response类和ApiException类。
  7. 返回结果规范:django rest framework Response类。
  8. Hypermedia API:django rest framework reverse方法

对于一个资源,我们的API几乎都是围绕资源的增、删、查、改四个操作开发的,为了数据安全,对于用户数据模型应只开放创建用户、查询用户信息和更新用户API对于删除用户,只能通过管理后端由管理员来处理,django建议不直接删除用户,而是将用户置为不活跃状态。下面实现我们的资源API,看看如何在具体设计中应用以上原则。

2.资源API视图

资源API视图以实现产品(KaopuShopProduct)的增删查改为例进行开发。在kaopu_shop应用文件夹下新建一个kaopu_shop_views包,在下面新建一个product_views.py,然后编写视图响应。

Django 架设 Restful API(五)API开发:资源API实现_第1张图片

product_views.py

from django.core.exceptions import ValidationError, ObjectDoesNotExist
from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from KaopuBackendDoNotDelete.restful_api_settings.api_exception_handle import KaopuParseError, KaopuNotFoundError, \
    KaopuValueError, KaopuSuccessResponse
from ..kaopu_shop_models.kaopu_shop_product import KaopuShopProduct, KaopuShopProductSerializer
from ..kaopu_shop_responses.kaopu_shop_responses import KaopuShopProductResponse


class KaopuProductViewSet(viewsets.ModelViewSet):
    """
    产品资源API
    """
    # 默认数据集
    queryset = KaopuShopProduct.objects.all()
    # 序列化器
    serializer_class = KaopuShopProductSerializer
    # 访问权限
    permission_classes = (IsAuthenticated, TokenHasReadWriteScope)

    def list(self, request, *args, **kwargs):
        """
        GET: 获取产品列表,序列化器加入context={"request": request}参数将返回绝对URL
        :param request:
        :param args:
        :param kwargs:
        :return: 产品列表
        """
        try:
            serializer = KaopuShopProductSerializer(self.queryset, many=True, context={"request": request})
            return KaopuShopProductResponse(data=serializer.data, state=request.query_params['state'])
        except KaopuValueError as e:
            return Response(e.get_full_details(), status=e.status_code)
        except KeyError:
            e = KaopuValueError()
            return Response(e.get_full_details(), status=e.status_code)

    def create(self, request, *args, **kwargs):
        """
        POST:创建新产品
        :param request:
        :param args:
        :param kwargs:
        :return: 新创建的产品信息
        """
        try:
            serializer = KaopuShopProductSerializer(data=request.data, context={"request": request})
            if serializer.is_valid(raise_exception=True):
                new_product = serializer.create(serializer.validated_data)
                return Response(KaopuShopProductSerializer(new_product).data)
            else:
                return Response(serializer.errors,
                                status=status.HTTP_400_BAD_REQUEST)
        except ValidationError as e:
            error = KaopuParseError(detail=e.messages)
            return Response(error.get_full_details(), status=error.status_code)
        except KaopuValueError as e:
            # rest_framework 验证错误
            return Response(e.get_full_details(), status=e.status_code)
        except KeyError:
            e = KaopuValueError()
            return Response(e.get_full_details(), status=e.status_code)

    def retrieve(self, request, pk=None, *args, **kwargs):
        """
        GET:获取新产品
        :param request:
        :param pk: 主键(product_id)
        :param args:
        :param kwargs:
        :return:
        """
        try:
            product = KaopuShopProduct.objects.get(product_id=pk)
            serializer = KaopuShopProductSerializer(product, context={"request": request})
            return KaopuShopProductResponse(data=serializer.data, state=request.query_params['state'])
        except ValidationError as e:
            error = KaopuParseError(detail=e.messages)
            return Response(error.get_full_details(), status=error.status_code)
        except ObjectDoesNotExist:
            not_found_error = KaopuNotFoundError()
            return Response(not_found_error.get_full_details(), status=not_found_error.status_code)
        except KaopuValueError as e:
            # rest_framework 验证错误
            return Response(e.get_full_details(), status=e.status_code)
        except KeyError:
            e = KaopuValueError()
            return Response(e.get_full_details(), status=e.status_code)

    def update(self, request, pk=None, *args, **kwargs):
        """
        PUT: 更改产品,客户端提供全部参数
        :param request:
        :param pk: 主键(product_id)
        :param args:
        :param kwargs:
        :return:
        """
        try:
            product = KaopuShopProduct.objects.get(product_id=pk)
            serializer = KaopuShopProductSerializer(data=request.data)
            if serializer.is_valid(raise_exception=True):
                modified_product = serializer.update(product, serializer.validated_data)
                return KaopuShopProductResponse(
                    data=KaopuShopProductSerializer(modified_product, context={"request": request}).data,
                    state=request.query_params['state'])
            else:
                return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        except ValidationError as e:
            error = KaopuParseError(detail=e.messages)
            return Response(error.get_full_details(), status=error.status_code)
        except ObjectDoesNotExist:
            not_found_error = KaopuNotFoundError()
            return Response(not_found_error.get_full_details(), status=not_found_error.status_code)
        except KaopuValueError as e:
            # rest_framework 验证错误
            return Response(e.get_full_details(), status=e.status_code)
        except KeyError:
            e = KaopuValueError()
            return Response(e.get_full_details(), status=e.status_code)

    def partial_update(self, request, pk=None, *args, **kwargs):
        """
        PATCH: 更新产品部分信息,客户端只需提供部分参数
        :param request:
        :param pk: 主键(product_id)
        :param args:
        :param kwargs:
        :return:
        """
        try:
            product = KaopuShopProduct.objects.get(product_id=pk)
            serializer = KaopuShopProductSerializer(data=request.data, partial=True)
            if serializer.is_valid(raise_exception=True):
                modified_product = serializer.update(product, serializer.validated_data)
                return KaopuShopProductResponse(
                    data=KaopuShopProductSerializer(modified_product, context={"request": request}).data,
                    state=request.query_params['state'])
            else:
                return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        except ValidationError as e:
            error = KaopuParseError(detail=e.messages)
            return Response(error.get_full_details(), status=error.status_code)
        except ObjectDoesNotExist:
            not_found_error = KaopuNotFoundError()
            return Response(not_found_error.get_full_details(), status=not_found_error.status_code)
        except KaopuValueError as e:
            # rest_framework 验证错误
            return Response(e.get_full_details(), status=e.status_code)
        except KeyError:
            e = KaopuValueError()
            return Response(e.get_full_details(), status=e.status_code)

    def destroy(self, request, pk=None, *args, **kwargs):
        """
        DELETE: 删除单个产品
        :param request:
        :param pk: 主键(product_id)
        :param args:
        :param kwargs:
        :return:
        """
        try:
            product = KaopuShopProduct.objects.get(product_id=pk)
            product.delete()
            success_response = KaopuSuccessResponse()
            return Response(success_response.get_full_details(), status=success_response.status_code)
        except ValidationError as e:
            error = KaopuParseError(detail=e.messages)
            return Response(error.get_full_details(), status=error.status_code)
        except ObjectDoesNotExist:
            not_found_error = KaopuNotFoundError()
            return Response(not_found_error.get_full_details(), status=not_found_error.status_code)
        except KeyError:
            e = KaopuValueError()
            return Response(e.get_full_details(), status=e.status_code)

    @action(detail=False, methods=['post'], url_name='kaopu_shop_product_batches_destroy',
            url_path='batches_destroy')
    def batches_destroy(self, request):
        """
        POST: 批量删除,传入主键列表字符串,用英文逗号分隔,如:{"pk_list": "1,2,3"}
        :param request:
        :return:
        """
        try:
            # 要删除的产品主键列表
            pk_list = str(request.data['pk_list']).split(',')
            products = KaopuShopProduct.objects.filter(pk__in=pk_list)
            products.delete()
            success_response = KaopuSuccessResponse()
            return Response(success_response.get_full_details(), status=success_response.status_code)
        except KeyError:
            error = KaopuParseError(detail='Invalid params,please check your request.')
            return Response(error.get_full_details(), status=error.status_code)
        except ValidationError as e:
            error = KaopuParseError(detail=e.messages)
            return Response(error.get_full_details(), status=error.status_code)

3.自动生成API 路径(URL)

在kaopu_shop文件夹下新建一个urls.py文件,注册资源API路径:

Django 架设 Restful API(五)API开发:资源API实现_第2张图片

urls.py

from django.urls import path, include
from rest_framework import routers

from kaopu_shop.kaopu_shop_views.product_views import KaopuProductViewSet

router = routers.SimpleRouter()
router.register(r'products', KaopuProductViewSet)
urlpatterns = router.urls

urlpatterns = [
    path('', include(router.urls)),
]

这样将自动生成我们的资源API URL

4.资源API权限控制

权限控制主要指配置Viewsetpermission_class属性,通常是一个列表或者是元组,可选值见https://www.django-rest-framework.org/api-guide/permissions/。该属性可以控制允许什么样的用户访问资源API。

5.资源API 过滤器

过滤器参见:https://django-filter.readthedocs.io/en/latest/ref/filters.html#field-name

添加django_filters到Django的INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    'django_filters',
    ...
]

将过滤器后端添加到设置中:

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}

然后创建过滤器类和过滤规则,这里仅实现常见的几种情况(参见2.4(6))。

5.1 字段匹配

首先定义过滤器类

from django_filters import rest_framework as filters, ChoiceFilter
from rest_framework import serializers

Product_KINDS = (
    ('A', "类型A"), ('A', "类型A"), ('C', "类型C"), )

class KaopuShopProduct(models.Model):
    ......


class ProductFilter(filters.FilterSet):
    # 产品类型过滤
    product_type = filters.ChoiceFilter(choices=Product_KINDS, field_name="product_type")
    # 价格下限
    price_gte = filters.NumberFilter(field_name="price", lookup_expr='gte')
    # 价格上限
    price_lte = filters.NumberFilter(field_name="price", lookup_expr='lte')

    class Meta:
        model = KaopuShopProduct
        fields = ['price', 'product_type', 'price_gte', 'price_lte', 'product_name']

因为过滤器类和数据模型密切相关,因此建议将其和数据模型放在一个py文件下,方便对照和管理。定义完过滤器类,还需要在ViewSet 中启用它们KaopuProductViewSet中添加以下代码

 class KaopuProductViewSet(viewsets.ModelViewSet):
    .....  
    filter_backends = (filters.DjangoFilterBackend,)
    filterset_class = ProductFilter

    def filter_queryset(self, queryset):
        # Other condition for different filter backend goes here
        for backend in list(self.filter_backends):
            queryset = backend().filter_queryset(self.request, queryset, view=self)
        return queryset
    
    ......

更多过滤选项参看django-filter文档进行配置。

5.2 API数据分页

数据分页参见:https://www.django-rest-framework.org/api-guide/pagination/

(1)配置全局分页样式

在restful_api_settings.py中添加设置:

REST_FRAMEWORK = {
    ......
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'PAGE_SIZE': 10,
    ......
}

这里使用的是偏移分页样式,即使用通过当前数据页+数据页偏移确定要加载的数据页。

(2)重写filter_queryset方法

 def filter_queryset(self, queryset):
        # Other condition for different filter backend goes here
        for backend in list(self.filter_backends):
            queryset = backend().filter_queryset(self.request, queryset, view=self)
        # 筛选结束后数据分页
        return self.pagination_class().paginate_queryset(queryset, self.request, view=self)

(3) 自定义分页样式

继承内置的数据分页类:

class LargeResultsSetPagination(PageNumberPagination):
    page_size = 1000
    page_size_query_param = 'page_size'
    max_page_size = 10000

然后在要使用分页的Viewset中设置pagination_class 属性,同样要重写filter_queryset方法。

6.API 版本控制

客户端指定API版本通常有三种方式:Accept标头中将版本号指定为媒体类型的一部分、将版本号指定为URL路径的一部分、将版本号作为URL中的查询参数。通常推荐将版本号指定为URL路径的一部分,因为这样更清晰明了。

6.1 在restful_api_settings.py中添加设置

REST_FRAMEWORK = {
    ......
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
    'DEFAULT_VERSION': 'v1',  # 默认的版本
    'ALLOWED_VERSIONS': ['v1','v2'],  # 有效的版本
    'VERSION_PARAM': 'version',  # 版本的参数名与URL_conf中一致
    ......
}

6.2 在kaopu_shop应用的urls.py中添加版本参数:

router = routers.SimpleRouter()
router.register(r'^(?P[v1丨v2]+)/products', KaopuProductViewSet)
urlpatterns = router.urls

urlpatterns = [
    path('', include(router.urls)),
]

6.3 URL携带版本号

http://localhost:8000/kaopushop/v1/products/

6.4 视图中区分请求版本

以后在视图中就可以通过访问 request.version 来获取当前请求的具体版本,然后根据不同的版本来返回不同的内容,例如之前的Viewset可以改成:


class KaopuProductViewSet(viewsets.ModelViewSet):
    def list(self, request, *args, **kwargs):
        """
        GET: 获取产品列表,序列化器加入context={"request": request}参数将返回绝对URL
        :param request:
        :param args:
        :param kwargs:
        :return: 产品列表
        """
        try:
            serializer = KaopuShopProductSerializer(self.filter_queryset(self.queryset),
                                                    many=True, context={"request": request})
             if request.version=='v1':
                 return KaopuShopProductResponseV1(data=serializer.data, state=request.query_params['state'])
             if request.version=='v2':
                 return KaopuShopProductResponseV2(data=serializer.data, state=request.query_params['state'])
        except KaopuValueError as e:
            return Response(e.get_full_details(), status=e.status_code)
        except KeyError:
            e = KaopuValueError()
            return Response(e.get_full_details(), status=e.status_code)

7.API节流设置

节流参见:https://www.django-rest-framework.org/api-guide/throttling/

API节流的目的是,对于某些特别耗费资源的API,需要限制其访问频率,避免服务器过载而宕机;对于一些短期内发起大量请求的恶意用户,要限制其访问,保护服务器安全;或者限制非VIP用户的带宽。在restful_api_settings.py中添加节流策略:

REST_FRAMEWORK = {
    ......,
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.ScopedRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'contacts': '1000/day',
        'uploads': '20/day'
    },
    ......
}

频率描述可以使用secondminutehourday。也可以使用Viewsetthrottle_classes来单独为某个Viewset设置节流策略,实现更细粒度的节流控制。

下一节:Django 架设 Restful API(六)项目部署:静态文件部署和CDN使用

https://blog.csdn.net/anbuqi/article/details/115280213

你可能感兴趣的:(django,django,restful)