掌握Django(八)

本文介绍高级API相关知识, 包括基于类的视图, Minxins, 通用视图, 视图集等相关实现视图的方法, 介绍了路由、嵌套路由的相关知识, 并介绍了如何进行API的过滤、搜索、分页等方面的知识点.


基于类的视图

前面我们介绍的视图均基于函数,通过条件语句判断客户端的请求方法,稍复杂的会有嵌套的if语句,比较丑.RESTFramework提供了基于类的视图,可以使代码清洁可读.这个类就是APIView.类里的方法get(),post()等就表request的method.

我们用APIView来改造视图, store–views.py:

from rest_framework.views import APIView

class ProductList(APIView):

    def get(self, request):

        queryset = Product.objects.all()

        serializer = ProductSerializer(queryset, many=True, context={

        ‘request’: request

        })

        return Response(serializer.data)

    def post(self, request):

        serializer = ProductSerializer(data=request.data)

        serializer.is_valid(raise_exception=True)

        serializer.save()

        return Response(serializer.data, status=status.HTTP_201_CREATED)

然后, 再在store–urls.py里修改映射:

path(‘products/<int:id>’, views.ProductDetail.as_view()),

我们注意到, 类与url的映射需要调用as_view方法.

同样的, 我们也改造 product_detail 为 ProductDetail类, 代码类似不具体列了.


Mixins

我们看到, 序列化展示, 或反序列化接收数据保存, 不同的对象的同一类操作都是类似的,不同点就是两个, 一是对象(或查询集)的不同,或序列化的类不同. 我们是可以将相关类似的操作封装的. mixins就是用来简化操作的. 

from rest_framework.mixins import CreateModelMixin, ListModelMixin, DestroyModelMixin, RetrieveModelMixin, UpdateModelMixin

是各个操作模式的实现与我们之前的代码是类似的, 可以按住 command(windows里按住ctrl), 再点击具体的类, 比如 CreateModeMinin, 就可以跳到该类的具体实现了. 我们可以逐个查看下他们的实现.


通用视图

实际操作我们不直接使用Mixin, 而是使用通用视图.

https://django-rest-framework.org/api-guide/generic-views/

有ListCreateAPIView, RetriveUpdateDestoryAPIView等.我们来简化下ProductList.

from rest_framework.generics import ListCreateAPIView

classProductList(ListCreateAPIView):

    def get_queryset(self):

        return Product.objects.all()

    def get_serializer_class(self):

        return ProductSerializer

    defget_serializer_context(self):

        return {“request”: self.request}

如同前面提到的, 各相同的逻辑都已封装,只需重写获取查询集, 重写获取序列化类 get_serializer_class, 注意只需返回类, 而非对象. 以及可选的上下文 get_serializer_context(我们前面用到了上下文)

还可以再简化, 如 get_queryset方法只是返回一个对象, 没有什么逻辑,我们可以用queryset代替. 同样的, serializer_class属性也可以代替方法 get_serializer_class.这样就简化成如下:

class ProductList(ListCreateAPIView):

    queryset = Product.objects.all()

    serializer_class = ProductSerializer

    def get_serializer_context(self):

        return {“request”: self.request}

我们再来修改下CollectionList:

class CollectiionList(ListCreateAPIView):

    queryset = Collection.objects.annotate(

        products_count=Count(‘products’))

    serializer_class = CollectionSerializer

products_count是统计出来的,在POST时是不需要提供的, 但这里如果不提供的话会提示缺少必要信息, 我们需要设置该字段的 read_only属性.

class CollectionSerializer(serializers.ModelSerializer):

    …

    products_count = serializers.IntegerField(read_only=True)


自定义通用视图

我们来修改下ProductDetail,继承RetrieveUpdateDestroyAPIView, 可以按住command, 点击查看该类的方法, 有 get、put、patch、delete等, 如果需要自定义的话,就可以重载方法, 我们重载delete方法如下:

class ProductDetail(RetrieveUpdateDestroyAPIView):

    queryset = Product.objects.all()

    serializer_class = ProductSerializer

    def delete(self, request, id):

        …

        return Response(status=status.HTTP_204_NO_CONTENT)

保存,刷新网页, 提示lookup_field不对,默认实现 id 名称应为 pk, 我们在urls.py里映射, 以及delete方法里都修改成pk , 再次保存成功.

def delete(self, request, pk):

    …

以及:

path(‘products/<int:pk>’, views.ProductDetail.as_view()),

如果我们不想修改成pk, 也可以设置lookup_field属性即可.

class ProductDetail(RetrieveUpdateDestroyAPIView):

    …

    lookup_field = ‘id’


视图集

用通用视图还是有重复的, 我们看下ProductList 和 ProductDetail, 都需要设置queryset 和 serializer_class, 可以用视图集ViewSets合并去重.

from rest_framework import viewsets

class ProductViewSet(viewsets.ModelViewSet):

queryset = Product.objects.all()

serializer_class = ProductSerializer

def get_serializer_context(self):

    return {“request”: self.request}

defdelete(self, request, pk):

    …

我们将Product的两个视图类变成一个ViewSet类. 同样的, 我们将Collection的两个视图类也变成一个CollectionViewSet. 

class CollectionViewSet(viewsets.ModelViewSet):

    queryset = Collection.objects.annotate(

        products_count=Count(‘products’)).all()

    serializer_class = CollectionSerializer

def delete(self,request,pk):

    collection = get_object_or_404(Collection, pk=pk)

    if collection.products.count() > 0:

        …

    collection.delete()

    return Response(status=status.HTTP_204_NO_CONTENT)

ModelViewSet默认情况包含所有方法,有时我们不希望增删改,而是只读的话, 可以继承 ReadOnlyModelViewSet 即可.

我们保存, 刷新网页结果是崩溃了, 因为之前的每个视图都有对应的url映射,但合并成单个ViewSet后, 需要设置url路由才能正确识别不同的url. 下一节介绍.


路由 Routers

前面说到, ViewSet的url模式,需要用到路由自动匹配, 我们修改store–urls.py:

from rest_framework.routers import SimpleRouter

router = SimpleRouter()

router.register(“products”, views.ProductViewSet)

router.register(“collections”, views.CollectionViewSet)

pprint(router.urls)

导入 SimpleRouter, 然后将ProductViewSet和CollectionViewSet注册进去,我们仅在控制台上查看, 能看到是这样的url正则表达式匹配模式:

[<URLPattern ‘^products/$’ [name=’product-list’]>,

 <URLPattern ‘^products/(?P<pk>[^/.]+)/$’ [name=’product-detail’]>,

 <URLPattern ‘^collections/$’ [name=’collection-list’]>,

 <URLPattern ‘^collections/(?P<pk>[^/.]+)/$’ [name=’collection-detail’]>]

说明router自动生成相关匹配模式, 我们将urls赋给urlpatterns:

urlpatterns = router.urls

然后刷新网页, 能看到与之前是一样的效果了. 如果除了用router自动匹配模式, 还要手动加入其他模式的话, 就需要用 include包括这个router.urls, 再在列中添加其他自定义的url模式.

from django.urls import path, include

urlpatterns = [

    path(”, include(router.urls)),

    # 添加其他模式,比如:

    # path(‘products/’, views.ProductList.as_view()),

    …

]

除了SimpleRouter, 还可以继承DefaultRouter类, 它相比SimpleRouter增加2个特性. 一是当我们访问 http://127.0.0.1/store时会给出提示链接, 我们可以直接点击进入; 二是我们可以加上json后缀,会自动给出原始数据,如 http://127.0.0.1:8000/store/products.json

查看列表页面,如http://127.0.0.1:8000/store/products,我们发现也有DELETE按钮,这是不对的,DELETE按钮应该只对特定的对象才有效, 我们需要修正.

这个错误是由于将ProductList和ProductDetail合并成ProductViewSet,而在ProductViewSet写了delete方法引起的, ViewSet除继承mixin几个类没有任何其他实现, 查看父类之一DestroyModelMixin 发现实际应该重载的方法应该是destroy,而destroy方法与我们之前重写的delete方法是类似的, 其中差别就在于判断是否存在orderItem该Product的逻辑. 再查看 RetrieveUpdateDestroyAPIView 的实现,它的delete方法也只是简单调用了destroy方法.我们删除重载的delete方法, 用destroy代替:

class ProductViewSet(viewsets.ModelViewSet):

    …

    def destroy(self, request, *args, **kwargs):

        if OrderItem.objects.filter(product__id=kwargs[‘pk’]).count() > 0:

           …

        return super().destroy(request, *args, **kwargs)

同样的, 我们对CollectionViewSet也作相同修改:

class CollectionViewSet(viewsets.ModelViewSet):

    …

    def destroy(self, request, *args, **kwargs):

        if Product.objects.filter(collection__id=kwargs[‘pk’]).count() > 0:

            …

    return super().destroy(request, *args, **kwargs)


建立商品评价API

添加一个模型, 允许对商品进行评价,通过下面这种方式访问到具体的评价信息

http://<url>/store/products/1/reviews/1

显然,如果用ViewSet的话,就需要用到㠌套的的路由,但第一步,我们先创建模型,创建迁移并迁移到数据库, 创建到序列化类以及视图集.

store– models.py:

class Review(models.Model):

    product = models.ForeignKey(

        Product, on_delete=models.CASCADE, related_name=’reviews’)

    name = models.CharField(max_length=255)

    description = models.TextField()

    date = models.DateField(auto_now_add=True)

迁移:

python manage.py makemigrations

python manage.py migrate

再在 store–serializers.py里添加:

class ReviewSerializer(serializers.ModelSerializer):

    class Meta:

        model = Review

        fields = [‘id’, ‘date’, ‘name’, ‘description’, ‘product’]

然后, 在store–views.py里添加:

class ReviewViewSet(viewsets.ModelViewSet):

    queryset = Review.objects.all()

    serializer_class = ReviewSerializer

下一步, 就是设置㠌套路由, 下一节介绍.


㠌套路由

㠌套路由需要用到drf-nested-routers包. 安装下:

pipenv install drf-nested-routers

具体查看: https://github.com/alanjds/drf-nested-routers

修改 store–urls.py:

from rest_framework_nested import routers

router = routers.DefaultRouter()

router.register(r”products”, views.ProductViewSet)

router.register(“collections”, views.CollectionViewSet)

products_router = routers.NestedDefaultRouter(

    router, r’products’, lookup=’product’)

products_router.register(r’reviews’, views.ReviewViewSet,

    basename=’product-reviews’)

urlpatterns = router.urls + products_router.urls

我们用rest_framework_nested 的routers代替rest_framework的routers,然后按官方文档注意两个路由:router和products_router.其中router是products_router的父路由,lookup字段表示父级的关联字段,实际会加上pk,比如这里设了product会查看‘product_pk’字段.

保存测试一下,可以新建、删除review等.

我们还可以优化,比如新建review时,我们访问…/products/1/reviews并POST,还需要提供product,而这个product_id(1)在url里已有了,我们改造下.

首先将 ReviewSerializer的fields属性字段去掉 product.

然后,需要将上下文的product_id信息传递给reviews,否则新建reviews就缺少必要的product信息.

store–views.py:

class ReviewViewSet(viewsets.ModelViewSet):

    def get_serializer_context(self):

        return {‘product_id’: self.kwargs[‘product_pk’]}

再在 store– serializers.py里添加:

class ReviewSerializer(serializers.ModelSerializer):

    …

    def create(self, validated_data):

        product_id = self.context[‘product_id’]

        return Review.objects.create(product_id=product_id, **validated_data)

测试OK.

还有一个问题,我们刚才product为1的商品添加了review,但我们访问product为2时的reviews:

http://127.0.0.1:8000/store/products/2/reviews/

也能看到product为1的review,需要过滤, 在 ReviewViewSet里去掉queryset属性, 而使用get_queryset方法过滤.

class ReviewViewSet(viewsets.ModelViewSet):

    …

    def get_queryset(self):

        return Review.objects.filter(product=self.kwargs[‘product_pk’])


筛选

我们访问…/store/products这个API端点时,会得到全部的product的数据库记录,有时不想全部返回,筛选部分,比如只需要collection_id为1的product记录,像这样…/store/products/?collection_id=1, 下面来实现.

将ProductViewSet的属性queryset去掉,改用方法get_queryset:

class ProductViewSet(viewsets.ModelViewSet):

    …

    def get_queryset(self):

        queryset = Product.objects.all()

        collection_id = self.request.query_params.get(‘collection_id’)

        if collection_id is not None:

            queryset = queryset.filter(collection_id=collection_id)

        return queryset

保存,提示没有设置basename没有设置,我们修改 store–urls.py , 根据要求添加 products 的basename:

router.register(“products”, views.ProductViewSet, basename=’produccts’)

再保存,测试网页如 …/products/?collection_id=3 

OK了.   


通用筛选

前面筛选collection_id需要不少判断代码, 如果需要筛选多个字段的话, 就会比较麻烦且重复.可以使用第三方应用 Django-filter.

pipenv install django-filter

再在settings.py的INSTALL_APPS节点中添加应用django-filters

接着修改store–views.py的ProductViewSet:

from django_filters.rest_framework import DjangoFilterBackend

class ProductViewSet(viewsets.ModelViewSet):

    queryset = Product.objects.all()

    serializer_class = ProductSerializer

    filter_backends = [DjangoFilterBackend]

    filterset_fields = [‘collection_id’]

我们设置filter_backends为DjangoFilterBackend,这个后端使我们能够进行通用筛选, 再设置filterset_fields即可. 改回用queryset=Product.objects.all(),取消掉方法get_queryset的相关判断逻辑,最终结果也之前一样,但代码明显精简了.

再进一步,如果我们要增加筛选unit_price字段,但这个字段就不能用等于多少来筛选了,而是需要一个范围.比如大于或小于多少价格来筛选. 我们需要自定义filter类, 具体可以查看Django filter官方文档, 这里快速学习一下.

创建新文件 store–filters.py文件

from django_filters.rest_framework import FilterSet

from .models import Product

class ProductFilter(FilterSet):

    class Meta:

        model = Product

        fields = {

        ‘collection_id’: [‘extra’],

        ‘unit_price’: [‘gt’, ‘lt’]

        }

再在store–views.py里修改ProductViewSet的 filterset_fields为 filterset_class. 

class ProductViewSet(viewsets.ModelViewSet):

    …

    filterset_class = ProductFilter

保存, 查看网页. 我们看到用DjangoFilter还多了一个filters按钮, 很方便筛选.


搜索

使用SearchFilter, 如下

from rest_framework.filters import SearchFilter

class ProductViewSet(viewsets.ModelViewSet):

    …

    filter_backends = [DjangoFilterBackend, SearchFilter]

    …

    search_fields = [‘title’, ‘description’]

search_fields也可以使用关系, 比如 collection__title.

保存之后, 我们可以在 search框输入条件, 输入的文本是大小写不敏感的, 比如 coffee 可以搜索到 Coffee, 也可以多条件,用空格分隔,比如 “coffee 10oz”. 实际生成的在url的查询参数是 …products/?search=coffee+10oz


排序

排序用OrderingFilter, 与SearchFilter来自同一模块, 使用也非常简单.

/products/?ordering=-unit_price,last_update

from rest_framework.filters import SearchFilter, OrderingFilter

class ProductViewSet(viewsets.ModelViewSet):

    …

    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]

    …

    ordering_fields = [‘unit_price’, ‘last_update’]

保存之后, url可以使用类似下面的参数排序, 前面加 – 表示倒序.

…/products/?ordering=-unit_price,last_update


分页

导入PageNumberPagination 类, 然后添加 pagination_class属性.

from rest_framework.pagination import PageNumberPagination

class ProductViewSet(viewsets.ModelViewSet):

    …

    pagination_class = PageNumberPagination

再在settings.py中的REST_FRAMEWORK字典里添加键值对 ‘PAGE_SIZE’: 10 .

REST_FRAMEWORK = {

    ‘COERCE_DECIMAL_TO_STRING’: False,

    ‘PAGE_SIZE’: 10

}

保存, 查看效果, 发现已分页了, 而且API输出页面添加统计信息及上一页下一页的链接.

我们只在Product的视图集添加分页, 如果需要在所有的模型视图集都分页, 只需在settings.py里添加 DEFAULT_PAGINATION_CLASS. 

REST_FRAMEWORK = {

    …

    ‘DEFAULT_PAGINATION_CLASS’: “rest_framework.pagination.PageNumberPagination”,

    ‘PAGE_SIZE’: 10

}

除了 类, 还有一种分页类, 叫 LimitOffsetPagination, 可以设置测试下.

前面我们只在settings.py里添加 PAGE_SIZE键, 而未添加 DEFAULT_PAGINATION_CLASS 时 会有警告提示, 实际使用中也很可能不是所有视图集都需要分页的, 我们可以自定义分页类消除这个警告.

创建 store–pagination.py 文件, 内容:

from rest_framework.pagination import PageNumberPagination

class DefaultPagination(PageNumberPagination):

    page_size = 10

将 settings.py中的   ‘PAGE_SIZE’: 10  删除. 再回到 store–views.py, 修改 ProductViewSet.

class ProductViewSet(viewsets.ModelViewSet):

    …

    pagination_class = DefaultPagination


小结

本文介绍了高级API的相关知识, 包括基于类的视图, Minxins, 通用视图, 视图集等相关实现视图的方法, 介绍了路由、嵌套路由的相关知识, 并介绍了如何进行API的过滤、搜索、分页等方面的知识点.

下一篇计划设计和实现购物车的功能.  是前面知识的巩固和练习, 敬请期待.

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注