本文介绍高级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的过滤、搜索、分页等方面的知识点.
下一篇计划设计和实现购物车的功能. 是前面知识的巩固和练习, 敬请期待.