Contents
23.7.2. 用Django REST framework实现豆瓣API应用¶
1.序列化器和视图¶
1.1 序列化¶
1. Serializer和ModelSerializer序列化选择¶
我们对Django REST framework的两种序列化方式做一个总结:Serializer和ModelSerializer两种序列化方式中,前者比较容易理解,适用于新手;后者则在商业项目中被使用的更多,在实际开发中建议大家多使用后者。
许多教材中都将Django REST framework的Serializer和ModelSerializer,与Django的Form和ModelForm做对比,虽然二者相似,在优劣选择上却是不同的。Form虽然没有ModelForm效率高,但是ModelForm的使用增加了项目的耦合度,不符合项目解耦原则,所以Form比ModelForm更优(除了字段量过大的情况);
而ModelSerializer有Serializer所有的优点,同时并没有比Serializer明显的不足之外,所以ModelSerializer比Serializer更优。
ModelSerializer与常规的Serializer相同,但提供了:
自动推断需要序列化的字段及类型
提供对字段数据的验证器的默认实现
提供了修改数据需要用到的
.create()、.update()方法的默认实现
另外我们还可以在 fileds 列表里挑选出需要的数据,以便减小数据的体积。
2.Serializer序列化方式¶
serializers.py
from rest_framework import serializers
from .models import UserProfile, Book
class BookSerializer(serializers.Serializer):
title = serializers.CharField(required=True, max_length=100)
isbn = serializers.CharField(required=True, max_length=100)
author = serializers.CharField(required=True, max_length=100)
publish = serializers.CharField(required=True, max_length=100)
rate = serializers.FloatField(default=0)
views.py
class BookAPIView1(APIView):
"""
使用Serializer
"""
def get(self, request, format=None):
APIKey = self.request.query_params.get("apikey", 0)
developer = UserProfile.objects.filter(APIkey=APIKey).first()
if developer:
balance = developer.money
if balance > 0:
isbn = self.request.query_params.get("isbn", 0)
books = Book.objects.filter(isbn=int(isbn))
books_serializer = BookSerializer(books, many=True)
developer.money -= 1
developer.save()
return Response(books_serializer.data)
else:
return Response("兄弟,又到了需要充钱的时候!好开心啊!")
else:
return Response("查无此人啊")
3.ModelSerializer序列化方式¶
serializers.py
class BookModelSerializer(serializers.ModelSerializer):
class Meta:
model = Book
# 将整个表的所有字段都序列化
# fields = "__all__"
fields = ('title', 'isbn', 'author') #指定序列化某些字段
views.py
class BookAPIView2(APIView):
"""
使用ModelSerializer
"""
def get(self, request, format=None):
APIKey = self.request.query_params.get("apikey", 0)
developer = UserProfile.objects.filter(APIkey=APIKey).first()
if developer:
balance = developer.money
if balance > 0:
isbn = self.request.query_params.get("isbn", 0)
books = Book.objects.filter(isbn=int(isbn))
books_serializer = BookModelSerializer(books, many=True)
developer.money -= 1
developer.save()
return Response(books_serializer.data)
else:
return Response("兄弟,又到了需要充钱的时候!好开心啊!")
else:
return Response("查无此人啊")
4.HyperlinkedModelSerializer序列化方式¶
HyperlinkedModelSerializer 基本上与之前用的 ModelSerializer
差不多,区别是它自动提供了外键字段的超链接,并且默认不包含模型对象的
id 字段。
HyperlinkedModelSerializer与ModelSerializer有以下区别:
默认情况下不包括
id字段。它包含一个
url字段,使用HyperlinkedIdentityField。关联关系使用
HyperlinkedRelatedField,而不是PrimaryKeyRelatedField。
参考文献:
5.总结¶
ModelSerializer比Serializer是模型序列化的首选方案!
参考文献:
https://www.cnblogs.com/gengfenglog/p/14658470.html#_lab2_0_4
1.2 Django REST framework视图三层封装¶
1.2.1 mixins.ListModelMixin+GenericAPIView¶
使用mixins.ListModelMixin+generics.GenericAPIView对APIView进行一次封装,至少需要加一个get函数:
def get(self,request,*args,**kwargs):
return self.list(request,*args,**kwargs)
views.py
class BookMixinView1(mixins.ListModelMixin, generics.GenericAPIView):
queryset = Book.objects.all()
serializer_class = BookModelSerializer
def get(self, request, *args, **kwargs): # 如果这里不加get函数,代表默认不支持get访问这个api,所以必须加上
APIKey = self.request.query_params.get("apikey", 0)
developer = UserProfile.objects.filter(APIkey=APIKey).first()
if developer:
balance = developer.money
if balance > 0:
isbn = self.request.query_params.get("isbn", 0)
developer.money -= 1
developer.save()
self.queryset = Book.objects.filter(isbn=int(isbn))
return self.list(request, *args, **kwargs)
else:
return Response("兄弟,又到了需要充钱的时候!好开心啊!")
else:
return Response("查无此人啊")
1.3 generics.ListAPIView¶
generics.ListAPIView相对于mixins.ListModelMixin+generics.GenericAPIView而言,所谓的封装,就是封装了一个get函数罢了。
views.py
class BookMixinView2(generics.ListAPIView):
queryset = Book.objects.all()
serializer_class = BookModelSerializer
def get(self, request, *args, **kwargs):
APIKey = self.request.query_params.get("apikey", 0)
developer = UserProfile.objects.filter(APIkey=APIKey).first()
if developer:
balance = developer.money
if balance > 0:
isbn = self.request.query_params.get("isbn", 0)
developer.money -= 1
developer.save()
self.queryset = Book.objects.filter(isbn=int(isbn))
return self.list(request, *args, **kwargs)
else:
return Response("兄弟,又到了需要充钱的时候!好开心啊!")
else:
return Response("查无此人啊")
1.4 viewsets+Router¶
views.py
class IsDeveloper(BasePermission):
message = '查无此人啊'
def has_permission(self, request, view):
APIKey = request.query_params.get("apikey", 0)
developer = UserProfile.objects.filter(APIkey=APIKey).first()
if developer:
return True
else:
print(self.message)
return False
class EnoughMoney(BasePermission):
message = "兄弟,又到了需要充钱的时候!好开心啊!"
def has_permission(self, request, view):
APIKey = request.query_params.get("apikey", 0)
developer = UserProfile.objects.filter(APIkey=APIKey).first()
balance = developer.money
if balance > 0:
developer.money -= 1
developer.save()
return True
else:
return False
class BookModelViewSet(viewsets.ModelViewSet):
authentication_classes = []
permission_classes = [IsDeveloper, EnoughMoney]
queryset = Book.objects.all()
serializer_class = BookModelSerializer
def get_queryset(self):
isbn = self.request.query_params.get("isbn", 0)
books = Book.objects.filter(isbn=int(isbn))
queryset = books
return queryset
urls.py
from django.contrib import admin
from django.urls import path
from users.views import BookAPIView1, BookAPIView2, BookMixinView1, BookMixinView2, BookModelViewSet
from rest_framework.routers import DefaultRouter
from django.conf.urls import include
router = DefaultRouter()
router.register(r'apibook5', BookModelViewSet)
urlpatterns = [
path('admin/', admin.site.urls),
# Serializer
path('apibook1/', BookAPIView1.as_view(), name='book1'),
# ModelSerializer
path('apibook2/', BookAPIView2.as_view(), name='book2'),
# 用mixins.ListModelMixin+GenericAPIView的方式实现视图封装
path('apibook3/', BookMixinView1.as_view(), name='book3'),
# 用generics.ListAPIView的方式实现视图封装
path('apibook4/', BookMixinView2.as_view(), name='book4'),
# 用viewsets+Router的方式实现视图封装
path('', include(router.urls)),
]
使用Postman对API进行测试,用GET的方式访问:
http://127.0.0.1:8000/apibook1/?apikey=abcdefghigklmn&isbn=777777 http://127.0.0.1:8000/apibook2/?apikey=abcdefghigklmn&isbn=777777 http://127.0.0.1:8000/apibook3/?apikey=abcdefghigklmn&isbn=777777 http://127.0.0.1:8000/apibook4/?apikey=abcdefghigklmn&isbn=777777 http://127.0.0.1:8000/apibook5/?apikey=abcdefghigklmn&isbn=777777
1.2 视图¶
1.2.1 常规Django视图¶
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
class JSONResponse(HttpResponse):
"""
An HttpResponse that renders its content into JSON.
"""
def __init__(self, data, **kwargs):
content = JSONRenderer().render(data)
kwargs['content_type'] = 'application/json'
super(JSONResponse, self).__init__(content, **kwargs)
@csrf_exempt
def snippet_list(request):
"""
列出所有的code snippet,或创建一个新的snippet。
"""
if request.method == 'GET':
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return JSONResponse(serializer.data)
elif request.method == 'POST':
data = JSONParser().parse(request)
serializer = SnippetSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JSONResponse(serializer.data, status=201)
return JSONResponse(serializer.errors, status=400)
@csrf_exempt
def snippet_detail(request, pk):
"""
获取,更新或删除一个 code snippet。
"""
try:
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
return HttpResponse(status=404)
if request.method == 'GET':
serializer = SnippetSerializer(snippet)
return JSONResponse(serializer.data)
elif request.method == 'PUT':
data = JSONParser().parse(request)
serializer = SnippetSerializer(snippet, data=data)
if serializer.is_valid():
serializer.save()
return JSONResponse(serializer.data)
return JSONResponse(serializer.errors, status=400)
elif request.method == 'DELETE':
snippet.delete()
return HttpResponse(status=204)
urls.py
from django.conf.urls import url
from snippets import views
urlpatterns = [
url(r'^snippets/$', views.snippet_list),
url(r'^snippets/(?P<pk>[0-9]+)/$', views.snippet_detail),
]
1.2.2 APIview¶
1 基于函数的视图¶
views.py
from django.shortcuts import render
# Create your views here.
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import Article
from .serializers import ArticleListSerializer
@api_view(['GET', 'POST'])
def article_list(request):
if request.method == 'GET':
articles = Article.objects.all()
serializer = ArticleListSerializer(articles, many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = ArticleListSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view装饰器允许视图接收GET、POST请求,以及提供如405 Method Not Allowed等默认实现,以便在不同的请求下进行正确的响应。返回了
Response,该对象由 Django 原生响应体扩展而来,它可以根据内容协商来确定返回给客户端的正确内容类型。如果数据验证有误,还可以返回适当的状态码以表示当前的情况。
urls.py
from django.urls import path
from article import views
app_name = 'article'
urlpatterns = [
path('', views.article_list, name='list'),
]
示例代码
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
@api_view(['GET', 'POST'])
def snippet_list(request, format=None):
"""
列出所有的snippet,或创建一个新的snippet。
"""
if request.method == 'GET':
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = SnippetSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# 单个snippet对象相应的视图,并且用于获取,更新和删除这个snippet。
@api_view(['GET', 'PUT', 'DELETE'])
def snippet_detail(request, pk, format=None):
"""
获取,更新或删除一个 code snippet。
"""
try:
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
serializer = SnippetSerializer(snippet)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif request.method == 'DELETE':
snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
urls.py
from django.urls import path, include, re_path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
urlpatterns = [
re_path(r'^snippets/$', views.snippet_list),
re_path(r'^snippets/(?P<pk>[0-9]+)/$', views.snippet_detail),
]
urlpatterns = format_suffix_patterns(urlpatterns)
2 基于类的视图¶
传统 Django 中就有基于类的视图的存在,DRF 中自然也有。优点也都差不多,即实现功能的模块化继承、封装,减少重复代码。
views.py
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
from django.http import Http404
from .models import Article
from .serializers import ArticleDetailSerializer
class ArticleView(APIView):
""" 获取文章所有信息 查询全部、新增 """
def get(self, request,format=None):
books = Article.objects.all()
ser = ArticleDetailSerializer(instance=books, many=True)
return Response(ser.data)
def post(self, request,format=None):
ser = ArticleDetailSerializer(data=request.data)
if ser.is_valid():
# 直接保存,保存到哪个表里?需要重写save
ser.save()
return Response(ser.data)
return Response(ser.errors)
class ArticleDetail(APIView):
"""文章详情视图 单查 、删、改 """
def get_object(self, pk):
"""获取单个文章对象"""
try:
# pk 即主键,默认状态下就是 id
return Article.objects.get(pk=pk)
except:
raise Http404
def get(self, request, pk,format=None):
article = self.get_object(pk)
serializer = ArticleDetailSerializer(article)
# 返回 Json 数据
return Response(serializer.data)
def put(self, request, pk,format=None):
article = self.get_object(pk)
serializer = ArticleDetailSerializer(article, data=request.data)
# 验证提交的数据是否合法
# 不合法则返回400
if serializer.is_valid():
# 序列化器将持有的数据反序列化后,
# 保存到数据库中
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk,format=None):
article = self.get_object(pk)
article.delete()
# 删除成功后返回204
return Response(status=status.HTTP_204_NO_CONTENT)
urls.py
ffrom django.urls import path
from article import views
from rest_framework.urlpatterns import format_suffix_patterns
app_name = 'article'
urlpatterns = [
# 单查 、删、改
path('<int:pk>/', views.ArticleDetail.as_view(), name='detail'),
# 查询全部、新增
path('info/', views.ArticleView.as_view(), name='view'),
]
urlpatterns = format_suffix_patterns(urlpatterns)
1.2.3 通用视图¶
对数据的增删改查是几乎每个项目的通用操作,因此可以通过 DRF 提供的 Mixin 类直接集成对应的功能。
说白了就是对APIView进行了封装操作。
1.Mixin类¶
修改一下 ArticleDetail 视图:
views.py
from .models import Article
from .serializers import ArticleDetailSerializer
from rest_framework import mixins
from rest_framework import generics
class ArticleList(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Article.objects.all()
serializer_class = ArticleDetailSerializer
"""
列出所有文章和新增文章
"""
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
class ArticleDetail(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
"""文章详情视图、 单查 、删、改 """
queryset = Article.objects.all()
serializer_class = ArticleDetailSerializer
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
2.使用通用的基于类的视图¶
使用 Mixin 已经足够简单了,但我们还可以让它更简单:
from .models import Article
from .serializers import ArticleDetailSerializer
from rest_framework import mixins
from rest_framework import generics
class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
""" 单查 、删、改 """
queryset = Article.objects.all()
serializer_class = ArticleDetailSerializer
# 发送请求试试,功能和最开头那个继承 APIView 的视图是完全相同的。
class ArticleList(generics.ListCreateAPIView):
""" 查询全部、增加"""
queryset = Article.objects.all()
serializer_class = ArticleListSerializer
除了上述介绍的以外,框架还提供
ListModelMixin、CreateModelMixin等混入类或通用视图,覆盖了基础的增删改查需求。
1.2.4 视图集¶
大部分对接口的操作,都是在增删改查的基础上衍生出来的。既然这样,视图集就将这些通用操作集成在一起了。
1. ViewSet¶
view.py
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from quickstart.models import BookInfo
from quickstart.serializers import BookInfoSerializer
from rest_framework import viewsets
class BookInfoViewSet(viewsets.ViewSet):
"""
获取所有图书和单个图书信息
"""
def list(self, request):
queryset = BookInfo.objects.all()
serializer = BookInfoSerializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
queryset = BookInfo.objects.all()
book = get_object_or_404(queryset, pk=pk)
serializer = BookInfoSerializer(book)
return Response(serializer.data)
urls.py
from django.urls import path
from quickstart import views
urlpatterns = [
path('books/', views.BookInfoViewSet.as_view({'get': 'list'})),
path('books/<int:pk>', views.BookInfoViewSet.as_view({'get': 'retrieve'}))
]
2.ModelViewSet¶
这次我们使用了ModelViewSet类来获取完整的默认读写操作。
示例1
view.py
from article.models import Article
from article.serializers import ArticleSerializer
from article.permissions import IsAdminUserOrReadOnly
from rest_framework import viewsets
class ArticleViewSet(viewsets.ModelViewSet):
""" 全部查看、新增、删、改、查"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = [IsAdminUserOrReadOnly]
def perform_create(self, serializer):
"""
重写perform_create()
提供了视图集无法自行推断的用户外键字段。
"""
serializer.save(author=self.request.user)
由于使用了视图集,我们甚至连路由都不用自己设计了,使用框架提供的
Router 类就可以自动处理视图和 url 的连接。
urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from article import views
router = DefaultRouter()
router.register(r'article', views.ArticleViewSet)
urlpatterns = [
path('admin/', admin.site.urls),
# drf 自动注册路由
path('api/', include(router.urls))
# path('api/article/', include('article.urls', namespace='article')),
]
示例2
view.py
from quickstart.models import BookInfo
from quickstart.serializers import BookInfoSerializer
from rest_framework import viewsets
class BookInfoModelViewSet(viewsets.ModelViewSet):
"""
获取所有图书和单个图书信息的增删改查
"""
queryset = BookInfo.objects.all()
serializer_class = BookInfoSerializer
urls.py
from django.urls import path
from quickstart import views
urlpatterns = [
path('books/', views.BookInfoModelViewSet.as_view({'get': 'list', 'post': 'create'})),
path('books/<int:pk>',
views.BookInfoModelViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}))
]
访问多个序列化器
覆写 get_serializer_class() 方法可以根据条件而访问不同的序列化器:
class ArticleViewSet(viewsets.ModelViewSet):
...
def get_serializer_class(self):
if self.action == 'list':
return SomeSerializer
else:
return AnotherSerializer
这次我们使用了ModelViewSet类来获取完整的默认读写操作。
请注意,我们还使用@detail_route装饰器创建一个名为highlight的自定义操作。这个装饰器可用于添加不符合标准create/update/delete样式的任何自定义路径。
默认情况下,使用@detail_route装饰器的自定义操作将响应GET请求。如果我们想要一个响应POST请求的动作,我们可以使用methods参数。
detail_route``**已经从****DRF 3.8中删除了。新版可以使用**``actions
from rest_framework.decorators import action
@action(detail=True, methods=['post'])
def set_password(self, request, pk=None):
....
示例3
class SnippetViewSet(viewsets.ModelViewSet):
"""
此视图自动提供`list`,`create`,`retrieve`,`update`和`destroy`操作。
另外我们还提供了一个额外的`highlight`操作。
"""
from rest_framework.decorators import action
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly,)
@action(renderer_classes=[renderers.StaticHTMLRenderer])
def highlight(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
# 等效于如下
# SnippetList,SnippetDetail和SnippetHighlight视图类。等于ModelViewSet视图集。ModelViewSet封装了很多步骤
# class SnippetList(generics.ListCreateAPIView):
# queryset = Snippet.objects.all()
# serializer_class = SnippetSerializer
# permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly,)
#
# def perform_create(self, serializer):
# serializer.save(owner=self.request.user)
#
#
# class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
# queryset = Snippet.objects.all()
# serializer_class = SnippetSerializer
# permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly,)
#
#
# class SnippetHighlight(generics.GenericAPIView):
# queryset = Snippet.objects.all()
# renderer_classes = (renderers.StaticHTMLRenderer,)
#
# def get(self, request, *args, **kwargs):
# snippet = self.get_object()
# return Response(snippet.highlighted)
明确地将ViewSets绑定到URL
请注意,我们是如何通过将http方法绑定到每个视图所需的操作,从每个ViewSet类创建多个视图的。
现在我们将资源绑定到具体的视图中,我们可以像往常一样在URL conf中注册视图。
from django.urls import path, include, re_path
from rest_framework.urlpatterns import format_suffix_patterns
# from snippets import views
from snippets.views import SnippetViewSet, UserViewSet, api_root
from rest_framework import renderers
snippet_list = SnippetViewSet.as_view({
'get': 'list',
'post': 'create'
})
snippet_detail = SnippetViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
})
snippet_highlight = SnippetViewSet.as_view({
'get': 'highlight'
}, renderer_classes=[renderers.StaticHTMLRenderer])
user_list = UserViewSet.as_view({
'get': 'list'
})
user_detail = UserViewSet.as_view({
'get': 'retrieve'
})
urlpatterns = format_suffix_patterns([
re_path(r'^$', api_root),
re_path(r'^snippets/$', snippet_list, name='snippet-list'),
re_path(r'^snippets/(?P<pk>[0-9]+)/$', snippet_detail, name='snippet-detail'),
re_path(r'^snippets/(?P<pk>[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'),
re_path(r'^users/$', user_list, name='user-list'),
re_path(r'^users/(?P<pk>[0-9]+)/$', user_detail, name='user-detail')
])
urlpatterns += [
re_path(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]
使用路由器
因为我们使用的是ViewSet类而不是View类,我们实际上不需要自己设计URL。将资源连接到视图和url的约定可以使用Router类自动处理。我们需要做的就是使用路由器注册相应的视图集,然后让它执行其余操作。
这是我们重写的urls.py文件。
from django.urls import path, include, re_path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
from rest_framework.routers import DefaultRouter
# 创建路由器并注册我们的视图。
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet)
router.register(r'users', views.UserViewSet)
# API URL现在由路由器自动确定。
# 另外,我们还要包含可浏览的API的登录URL。
urlpatterns = [
re_path(r'^', include(router.urls)),
re_path(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
3.ReadOnlyModelViewSet¶
ReadOnlyModelViewSet类来自动提供默认的“只读”操作。
我们仍然像使用常规视图那样设置queryset和serializer_class属性,但我们不再需要向两个不同的类提供相同的信息。
view.py
from quickstart.models import BookInfo
from quickstart.serializers import BookInfoSerializer
from rest_framework import viewsets
class BookInfoReadOnlyModelViewSet(viewsets.ReadOnlyModelViewSet):
"""
获取所有图书和单个图书信息
"""
queryset = BookInfo.objects.all()
serializer_class = BookInfoSerializer
urls.py
from django.urls import path
from quickstart import views
urlpatterns = [
path('books/', views.BookInfoReadOnlyModelViewSet.as_view({'get': 'list'})),
path('books/<int:pk>', views.BookInfoReadOnlyModelViewSet.as_view({'get': 'retrieve'}))
]
总结
ViewSetMixin: 重写了as_view
ViewSet: 继承ViewSetMixin和APIView
GenericViewSet: 继承ViewSetMixin, generics.GenericAPIView
ModelViewSet: 继承mixins.CreateModelMixin,mixins.RetrieveModelMixin,mixins.UpdateModelMixin,mixins.DestroyModelMixin,mixins.ListModelMixin,GenericViewSet
# ModelViewSet 封装提供了增删改查、单查
源码:
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
"""
pass
ReadOnlyModelViewSet:继承mixins.RetrieveModelMixin,mixins.ListModelMixin,GenericViewSet
参考文献:
1.3 小结¶
那么,到底该怎样选择视图封装呢?我们马上就将得到一个相对确切的答案。
首先,我们来剖析视图的封装层数。要知道,我们经常说到的Django REST framework的“三层视图封装”,并不是仅仅封装了三层,下面解剖一个viewsets.ModelViewSet看一下:
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
class GenericAPIView(views.APIView):
可以看出,从APIView到views.ModelViewSet,mixins只是个过程,mixins存在的价值,更多的是为了帮助Django REST framework学习者,更加容易地理解视图封装的原理。
但事实上似乎并没有起到帮助作用。可以说,我们在今后的项目中,只需要优先在APIView和viewsets中选择即可。至于mixins就好像是斐波那契数列一样,几乎永远不会缺席于应聘Django REST framework技术岗位的笔试题中,但在实际项目中却很少能用得上。
APIView和viewsets
视图集最大程度地减少需要编写的代码量,并允许你专注于 API 提供的交互和表示形式,而不是 URL 的细节。但并不意味着用它总是比构建单独的视图更好。
原因就是它的抽象程度太高了。如果你对 DRF 框架的理解不深并且需要做某种定制化业务,可能让你一时间无从下手。
在精简和可读之间,你应该根据实际情况进行取舍。
APIView和viewsets应该怎样选择呢?Django REST framework的官方文档中也有介绍过二者的取舍问题,但帮助不大。viewsets虽然对APIView做了封装,如果出现代码结构更多,逻辑更麻烦,像这类情况,我们应该选择APIView。
如果仅仅做增删改查、CURD操作、无逻辑处理,直接使用视图集实现不需要多写一些重复代码。
总结一下:
当视图要实现的功能中,存在数据运算、拼接的业务逻辑时,比如本章例子中,API成功访问一次,用户表中的money记录减少1,可以一律选择APIView的方式来写视图类。
除此以外,优先使用viewsets的方式来写视图类,毕竟使用viewsets+Router在常规功能上效率极高。
2. 用Django设计大型电商的类别表¶
2.1 电商类别表的项目功能需求¶
(1)类别表必须包含多级类目,至少分为四级类目。
(2)类别表每一级类目数据都要有各自类目级别的标注,以便前端进行网页设计。
(3)类别表内的类别数据,必须可以灵活地进行增、减,并且不会因此而改变其上下层级的数据关系。
2.1.1 使用Vue.js在前端开发一个电商导航栏项目demo1¶
Vue.js就是一个前端框架。Vue.js的单页面应用模式对于前端技术的影响非常深远,包括微信小程序在内的很多前端原生语言,都是借鉴了此模式。
Vue.js是一个构建数据驱动的Web界面渐进式框架,与Angular和React并称为前端三大框架,而Vue.js框架是由华人尤雨溪所创造,开发文档更适合中国人阅读,而且尤雨溪也已经加盟阿里巴巴,所以Vue.js在国内也得到了阿里巴巴的推广,已经成为了国内最热门的前端框架之一。
Vue.js的知识非常简单,如果大家已经掌握了HTML+CSS+JavaScript语言,通过Vue.js的官方文档,只需要几个小时的学习,就可以轻松上手Vue.js框架了。Vue.js是实现多端并行中非常重要,也是非常基础的一个技术。
3. 搭建Vue.js前端开发环境¶
(1)下载Node.js。
Node.js官网地址为https://nodejs.org/en/
(2)安装Node.js。
(3)查看Node.js是否安装成功。
(4)安装淘宝镜像cnpm,可以让资源包下载得更快。
# 安装淘宝npm
npm install -g cnpm --registry=https://registry.npm.taobao.org
(5)安装Vue.js的脚手架工具。输入以下命令
# vue-cli 安装依赖包
cnpm install --g vue-cli
(6)创建项目。创建Vue.js项目,命名为vue02.然后连续按5次Enter键默认选项。
# vue init webpack-simple vue02
? Project name demo1
? Project description A Vue.js project
? Author
? License MIT
? Use sass? No
vue-cli · Generated "demo1".
To get started:
cd demo1
npm install
npm run dev
项目新建完成后,切换到项目目录下,安装依赖:
[root@cicd-dev lib]# cd vue02D:\vue02>cnpm install axios --save
/
[root@cicd-dev demo1]# cnpm install
(7)运行初始项目。在项目目录demo1下,执行以下命令,结果如图所示:
[root@cicd-dev demo1]# npm run dev
> vue02@1.0.0 dev /usr/local/src/node-v10.9.0-linux-x64/lib/demo1
> cross-env NODE_ENV=development webpack-dev-server --open --hot
Project is running at http://localhost:8086/
4. 使用PyCharm新建后端演示项目¶
4.1 新建基础环境和数据¶
(1)新建Django项目并命名为demo2,同时新建App,命名为app01。
(2)在PyCharm中打开项目终端,安装相关依赖包:
这里使用豆瓣源加速安装依赖包
pip install Djangorestframework markdown Django-filter pillow Django-guardian coreapi -i "https://pypi.doubanio.com/simple/"
(3)在demo2/demo2/settings.py中注册rest_framework:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app01.apps.App01Config',
'rest_framework'
]
(3)demo2连接MySQL数据库,在settings中将数据库配置代码修改为:
将原本的数据库配置代码注释掉,然后填入新的配置代码,否则后面填入的配置代码是不起作用的。
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': BASE_DIR / 'db.sqlite3',
# }
# }
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'demo2',
'USER': 'root',
'PASSWORD': 'OSChina@2020',
'HOST': '127.0.0.1',
# 第三方登录功能必须加上
"OPTIONS": {"init_command": "SET default_storage_engine=INNODB;"}
}
}
然后,安装PyMYSQL:
$ pip install PyMYSQL
在demo2/demo2/__init__.py中加入代码:
import pymysql
pymysql.install_as_MySQLdb()
(4)在demo2/app01/models.py中新建类别表:
from django.db import models
from datetime import datetime
# Create your models here.
class Type1(models.Model):
"""
一级类目
"""
name = models.CharField(max_length=10, default="", verbose_name="类目名")
add_time = models.DateTimeField(default=datetime.now, verbose_name='添加时间')
class Meta:
verbose_name = '商品类别'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Type2(models.Model):
"""
二级类目
"""
parent = models.ForeignKey(Type1, verbose_name="父级类别",
null=True, blank=True, on_delete=models.CASCADE)
name = models.CharField(max_length=10, default="", verbose_name="类目名")
add_time = models.DateTimeField(default=datetime.now, verbose_name='添加时间')
class Meta:
verbose_name = '商品类别2'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Type3(models.Model):
"""
三级类目
"""
parent = models.ForeignKey(Type2, verbose_name="父级类别",
null=True, blank=True, on_delete=models.CASCADE)
name = models.CharField(max_length=10, default="", verbose_name="类目名")
add_time = models.DateTimeField(default=datetime.now, verbose_name='添加时间')
class Meta:
verbose_name = '商品类别3'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Type4(models.Model):
"""
四级类目
"""
parent = models.ForeignKey(Type3, verbose_name="父级类别", null=True, blank=True, on_delete=models.CASCADE)
name = models.CharField(max_length=10, default="", verbose_name="类目名")
add_time = models.DateTimeField(default=datetime.now, verbose_name='添加时间')
class Meta:
verbose_name = '商品类别4'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
然后同第(2)步一样,打开项目终端,执行数据更新命令:
$ python manage.py makemigrations
$ python manage.py migrate
当数据表构建成功后,在项目目录中会生成一个db.sqlite3数据库文件。
(5)手动添加一些数据。我们不需要其他的数据库操作软件
没有使用MySQL数据库,而是直接使用了Django自带的sqlite3数据库,的确方便了许多。
但是sqlite3的局限性是只适合数据量级比较小的数据库服务,一旦涉及数据量比较庞大的项目,就要选择使用MySQL数据库或者其他数据库。
4.2 完善demo2的后台逻辑代码¶
(1)在app01目录下新建序列化模块serializers.py,新建4个类别表的序列化类:
from rest_framework import serializers # 引入序列化模块
from .models import Type1, Type2, Type3, Type4 # 引入所有数据表类
class Type1ModelSerializer(serializers.ModelSerializer):
class Meta:
model = Type1
fields = "__all__"
class Type2ModelSerializer(serializers.ModelSerializer):
class Meta:
model = Type2
fields = "__all__"
class Type3ModelSerializer(serializers.ModelSerializer):
class Meta:
model = Type3
fields = "__all__"
class Type4ModelSerializer(serializers.ModelSerializer):
class Meta:
model = Type4
fields = "__all__"
(2)在app01/views.py中,编写访问4个类别表的视图逻辑代码:
#引入序列化类
from .serializers import Type1ModelSerializer,Type2ModelSerializer
from .serializers import Type3ModelSerializer,Type4ModelSerializer
#引入数据表
from .models import Type1,Type2,Type3,Type4
#引入rest_framework相关模块
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer, BrowsableAPIRenderer
# Create your views here.
class Type1View(APIView):
"""
all Type1
"""
renderer_classes = [JSONRenderer]
def get(self, request, format=None):
types=Type1.objects.all()
types_serializer = Type1ModelSerializer(types, many=True)
return Response(types_serializer.data)
class Type2View(APIView):
"""
all Type2
"""
renderer_classes = [JSONRenderer]
def get(self, request, format=None):
types=Type2.objects.all()
types_serializer = Type2ModelSerializer(types, many=True)
return Response(types_serializer.data)
class Type3View(APIView):
"""
all Type3
"""
renderer_classes = [JSONRenderer]
def get(self, request, format=None):
types=Type3.objects.all()
types_serializer = Type3ModelSerializer(types, many=True)
return Response(types_serializer.data)
class Type4View(APIView):
"""
all Type4
"""
renderer_classes = [JSONRenderer]
def get(self, request, format=None):
types=Type4.objects.all()
types_serializer = Type4ModelSerializer(types, many=True)
return Response(types_serializer.data)
注意: 这里用到了Django REST framework的选择器,大家可以根据自己的喜好,选择使用JSONRenderer模式还是BrowsableAPIRenderer.
renderer_classes = [JSONRenderer, BrowsableAPIRenderer] # 渲染器
(3)在demo2/urls.py中添加路由代码:
from django.contrib import admin
from django.urls import path
# 引入视图类
from app01.views import Type1View, Type2View, Type3View, Type4View
urlpatterns = [
path('admin/', admin.site.urls),
path('api/type1/', Type1View.as_view()),
path('api/type2/', Type2View.as_view()),
path('api/type3/', Type3View.as_view()),
path('api/type4/', Type4View.as_view())
]
5.前后端项目联合调试¶
现在,前端项目demo1和后端项目demo2都完成了,可以正式开始前后端联合调试的工作了。
(1)运行demo2项目。
(2)给vue02项目安装网络请求模块axios。在cmd窗口执行安装axios模块命令。
D:\vue02>cnpm install axios --save
(3)改写前端项目,在vue02/src/App.vue中,<style>样式标签内的代码不做改变,其他代码修改如下:
html
<template>
<div id="app">
<div class="all">
<div class="one">
<div class="onetype" v-for="(item,index) in one" :key="index">
<b>{{one[index].name}}</b>
</div>
</div>
<div class="twothreefour">
<div class="two">
<div class="twotype"
v-for="(item,index) in two" :key="index"
@mouseenter="open(index)">
<b>{{two[index].name}}</b>
</div>
</div>
<div class="threefour" v-if="flag"
@mouseleave="close()">
<div class="threefourtype" v-for="(item,index) in three1" :key="index">
<span class="three">{{three1[index]}}</span>
<span class="four" v-for="(item4,index4) in four1" :key="index4">
{{four1[index4]}} </span>
</div>
</div>
</div>
</div>
</div>
</template>
js
<script>
import Axios from 'axios';
export default {
name: 'app',
data() {
return {
one: [],
two: [],
three: [],
four: [],
flag: false,
three1: [],
four1: []
}
},
methods: {
getData() {
const api = 'http://127.0.0.1:8000/';
var api1 = api + 'api/type1/';
var api2 = api + 'api/type2/';
var api3 = api + 'api/type3/';
var api4 = api + 'api/type4/';
var type1 = [];
var type2 = [];
var type3 = [];
var type4 = [];
Axios.get(api1)
.then(function (response) {
// console.log(response);
for (var i = 0; i < response.data.length; i++) {
// console.log(response.data[i])
type1.push(response.data[i])
}
// console.log(type1)
})
.catch(function (error) {
console.log(error);
});
this.one = type1;
Axios.get(api2)
.then(function (response) {
// console.log(response);
for (var i = 0; i < response.data.length; i++) {
// console.log(response.data[i])
type2.push(response.data[i])
}
// console.log(type2)
})
.catch(function (error) {
console.log(error);
});
this.two = type2;
Axios.get(api3)
.then(function (response) {
// console.log(response);
for (var i = 0; i < response.data.length; i++) {
// console.log(response.data[i])
type3.push(response.data[i])
}
// console.log(type3)
})
.catch(function (error) {
console.log(error);
});
this.three = type3;
Axios.get(api4)
.then(function (response) {
// console.log(response);
for (var i = 0; i < response.data.length; i++) {
// console.log(response.data[i])
type4.push(response.data[i])
}
// console.log(type4)
})
.catch(function (error) {
console.log(error);
});
this.four = type4;
// console.log(this.one)
// console.log(this.two)
// console.log(this.three)
// console.log(this.four)
},
open(index) {
// console.log(this.two[index].id)
var temp = []
for (var i = 0; i < this.three.length; i++) {
if (this.three[i].parent === index) {
temp.push(this.three[i].name)
}
}
console.log(temp)
this.three1 = temp;
var temp4 = []
for (var j = 0; j < this.four.length; j++) {
temp4.push(this.four[j].name)
}
this.four1 = temp4
this.flag = true
},
close() {
this.flag = false
}
},
mounted() {
this.getData()
},
}
</script>
一级、二级、三级类目遵循了从属关系,而第四级类目为了体现效果并没有通过筛选赋值。
(4)解决跨域问题。在后端Django项目demo2中安装相关模块:
pip install Django-cors-headers -i "https://pypi.doubanio.com/simple/"
然后在settings.py中的注册里配置如下:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app01.apps.App01Config',
'rest_framework',
'corsheaders'
]
在settings.py中的MIDDLEWARE里设置如下:
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # 放到中间件顶部
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
在settings.py中新增配置项,即可解决本项目中的跨域问题。
CORS_ORIGIN_ALLOW_ALL = True
注意:在Python全栈开发的知识体系里,跨域问题和深浅拷贝,几乎是逢面试必考的两个笔试题。
不同的是,深浅拷贝在实际项目中很少用到,而跨域问题却几乎在每个项目中都有涉及,只是并非都能被察觉罢了。跨域问题是非常重要的一个知识点,关系到网络安全,甚至说跨域问题是Web安全中最重要的一环也不为过。
(5)重启前后端,即可看到效果
6. 使用Django的model实现类别表建立¶
6.1 四表合一¶
类别表本来就应该是一张表,我们来构建一张类别表,把上一节用传统建表方式建立的四张类别表合为一张。
(1)在demo2/app01/models.py中新建类Type。
class Type(models.Model):
"""
商品类别
"""
CATEGORY_TYPE=(
(1,'一级类目'),
(2,'二级类目'),
(3,'三级类目'),
(4,'四级类目')
)
name=models.CharField(default='',max_length=30,verbose_name='类别名',help_text='类别名')
code=models.CharField(default='',max_length=30,verbose_name='类别code',help_text='类别code')
desc=models.CharField(default='',max_length=30,verbose_name='类别描述',help_text='类别描述')
category_Type=models.IntegerField(choices=CATEGORY_TYPE,verbose_name='类别描述',help_text='类别描述')
parent_category=models.ForeignKey('self',null=True,blank=True,verbose_name='父类目录',help_text='父类别',related_name='sub_cat',on_delete=models.CASCADE)
is_tab=models.BooleanField(default=False,verbose_name='是否导航',help_text='是否导航')
class Meta:
verbose_name='商品类别'
verbose_name_plural=verbose_name
def __str__(self):
return self.name
(2)建表,执行数据更新命令如下:
python manage.py makemigrations
python manage.py migrate
6.2 数据导入¶
因为在上一节中建立的4个类别表已经手动加入了一些数据,我们没有必要再用手动加入的方式将这些记录加入新建的Type表里,而是选择使用一种更加方便的方式,即通过Postman将数据以post的方式,加入Type表内。
(1)在demo2/app01/serializers.py内增加Type表的序列化类
from .models import Type
class TypeModelSerializer(serializers.ModelSerializer):
class Meta:
model=Type
fields="__all__"
(2)在demo2/app01/views.py内创建TypeView视图类:
from .models import Type
# 导入序列化类
from .serializers import TypeModelSerializer
class TypeView(APIView):
"""
操作类别表
"""
renderer_classes = [JSONRenderer]
def get(self, request, format=None):
types = Type.objects.all()
types_serializer = TypeModelSerializer(types, many=True)
return Response(types_serializer.data)
def post(self, request):
name = request.data.get('name')
category_type = request.data.get('lei')
parent_category_id = request.data.get('parent')
type = Type()
type.name = name
type.category_type = category_type
if parent_category_id:
parent_category = Type.objects.filter(id=parent_category_id).first()
type.parent_category = parent_category
type.save()
type_serializer = TypeModelSerializer(type)
return Response(type_serializer.data)
注意: 这里用到了Django REST framework的选择器,大家可以根据自己的喜好,选择使用JSONRenderer模式还是BrowsableAPIRenderer.
renderer_classes = [JSONRenderer, BrowsableAPIRenderer] # //渲染器
全局使用
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': ['rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.BrowsableAPIRenderer'],
}
(3)在demo2/demo2/urls.py内增加路由代码如下:
from app01.views import TypeView
urlpatterns = [
#......
path('api/type/',TypeView.as_view())
]
(4)运行demo2项目,使用Postman通过post加入数据记录,
省略
7.前后端项目联合调试¶
崭新的数据类别表构建完成了,下面将前端项目改造一下,对接调试我们的类别表,看看可以节省多少前端的工作量。
(1)在demo1/src/App.vue原来的基础上,只修改<script>标签内的代码:
<script>
import Axios from 'axios';
export default {
name: 'app',
data () {
return {
type:[],
one:[],
two:[],
flag:false,
three1:[],
four1:[]
}
},
methods: {
getData(){
const api='http://127.0.0.1:8000/api/type/';
var _this=this
Axios.get(api)
.then(function (response) {
_this.type=response.data;
for(var i=0;i<_this.type.length;i++){
if(_this.type[i].category_type===1){
_this.one.push(_this.type[i])
}
}
for(var j=0;j<_this.type.length;j++){
if(_this.type[j].category_type===2){
_this.two.push(_this.type[j])
}
}
})
.catch(function (error) {
console.log(error);
});
},
open(index){
this.three1=[]
this.four1=[]
var parent=this.two[index].id
for(var i=0;i<this.type.length;i++){
if(this.type[i].parent_category===parent){
this.three1.push(this.type[i].name)
}
if(this.type[i].category_type===4){
this.four1.push(this.type[i].name)
}
}
this.flag=true
},
close(){
this.flag=false
}
},
mounted() {
this.getData()
}
}
</script>
(2)运行前端demo1和后端demo2,查看效果图,如图3-32所示。我们用四分之一的代码量,完成了一个更加强大的类别表。