"安居客"住房系统-基于Python-Django前后端分离开发(二)
基于Django-Rest-Framework创建接口数据(二)
文章目录
通过ORM框架,直接通过面向对象的方式对模型进行查询操作。由于模型都是objects的属性,它是模型对象的管理工具,提供了CRUD的方法。
省市区三级联动数据接口
创建省级行政区的数据接口
在api
文件中为其创建一个简单的序列化器DistrictSimpleSerializers
,只需要序列省级行政区的地区编号和名称。
在api
文件中新建serializers
文件,用来书写FBV
的序列化器
class DistrictSimpleSerializers(serializers.ModelSerializer):
class Meta:
model = District
fields = ('distid', 'name')
在common
的view.py
中编写查询省级行政区的视图函数get_provinces
@api_view(('GET', ))
def get_provinces(request: HttpRequest) -> HttpResponse:
queryset = District.objects.filter(parent__isnull=True)
serializer = DistrictSimpleSerializers(queryset, many=True)
return Response({
'code': 10000,
'message': '获取省级行政区成功',
'result': serializer.data
})
【说明】
- 判断父级行政区为空,需要用
isnull
属性而不能用parent=NULL
- 拿到
queryset
查询集,需要对其进行序列化操作。所谓序列化,就是把一个对象变成字符串或是字节串;反序列化,激素hi将一个字符串或是字节串还原成对象的过程。 - 此处由于返回Response,需要加上装饰器才能执行。
将Django数据迁移到数据库中,避免与浏览器缓存数据发生冲突,导致无法查询到数据接口。
# 在终端中执行命令
python manage.py migrate sessions
在api/urls.py
文件添加视图函数路由
urlpatterns = [
path('districts/', get_provinces)
]
如果希望请求途中包含api
,还需要配置全局url
,在Django项目的urls.py
文件中
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls'))
]
完成好以上操作,运行项目,在浏览器中请求api/districts
数据接口,就可以查询到行政区的JSON数据了
在页面左侧调试栏中的SQL语句中发现,除了查询distid
和name
,其他的字段也都被查询出来了,这将影响SQL语句的查询效率,这时需要在查询集添加only()
属性指定需要查询的字段,也可以使用defer()
指定不需要查询的字段,下文不再赘述。
queryset = District.objects.filter(parent__isnull=True).only('distid', 'name')
查询每个省的城市/区/县的信息
每个地区都有自己对应的编号,例如四川的编号为510000
,成都的编号为510100
,若要查询对应市/区的信息。首先配置api/urls.py
文件
path('districts/<int:distid>', get_cities)
当我们在浏览器中请求api/districts/510000
就可以拿到四川省各个城市的信息,如果请求510100
就能够拿到成都市各个区县的信息,所以接下来的视图函数需要返回各个城市的地区编号,将函数命名为get_cities
。由于需要根据传入的实参查询到地区编号,再根据地区编号查询到对应的地区,这里先来编写查询城市信息的序列化器。
class DistrictDetailSerializer(serializers.ModelSerializer):
cities = serializers.SerializerMethodField()
@staticmethod
def get_cities(district):
queryset = District.objects.filter(parent__distid=district.distid)
return DistrictSimpleSerializers(queryset, many=True).data
class Meta:
model = District
exclude = ('parent', 'ishot')
【说明】
parent__distid=district.distid
:找到父级行政区的distid
和传入的参数distid
相同的字段,返回父级行政区为parent__distid
的对象,如果参数为510100,则返回父级行政区为510100的城市,也就是返回510100对应的区县。
查询城市数据接口的视图函数如下:
@api_view(('GET', ))
@cache_page(timeout=600, cache='default')
def get_cities(request, distid):
district = District.objects.get(distid=distid) # 根据主键查询编号,通过编号拿到地区
serializer = DistrictDetailSerializer(district)
return Response(serializer.data)
我们同样将城市接入Redis数据库缓存,再进入浏览器中查询api/districts/510000
会看到如下信息。
查询api/districts/510100
会看到如下信息。
查询热门城市信息
虽然FBV
比较灵活,但是FBV
的api数据接口的代码量比较多,比较冗杂,接下来查询热门城市信息,我将会用CBV
j进行书写。
class HotCitiesView(ListAPIView):
queryset = District.objects.filter(ishot=True)
serializer_class = DistrictSimpleSerializers
因为我们需要的只是查询到热门城市的信息,而不是对其进行增删改操作,所以此时只需要传给类只做查询功能的ListAPIView
,此处只能返回一个列表。此处依然通过DistrictSimpleSerializers
就可以对查询到的热门城市进行序列化。由于HotCitiesView
是一个类而不是函数,所以在添加HotCitiesView
的url
映射时,会和函数稍有差别时。
path('hotcities/', HotCitiesView.as_view())
通过as_view()
可以将类直接变成视图函数。
修改好了之后,我们就可以请求http://127.0.0.1:8000/api/hotcities/
拿到热门城市的信息
但是此时返回的是一个列表而不是我们希望的字典,这时我们可以重写在ListAPIView
类中的get
方法,此方法返回的是一个响应对象,调用其父类的方法拿到返回值。在得到返回值之前,我们可以添加自己的业务逻辑。若要返回字典,可以自定义重新定制返回值。
# HotCitiesView类中添加函数
def get(self, request, *args, **kwargs):
resp = super().get(request, *args, **kwargs)
return Response({
'code': 10000,
'message': '获取热门城市数据成功',
'result': resp.data
})
(重点)设置经理人数据接口
此方法和返回热门城市的数据接口代码类似,详细代码间我的代码库 ,但是如果不想返回经理人,只想返回指定经理人的详细信息,这时可以在类中继承RetrieveAPIView
,用过其中的self.retrieve
获取单个对象。这时还需要调整urlpath('aagents/<int:pk>', AgentView.as_view())
,资源标识符<int:pk>
【说明:DRF框架默认参数pk
】。
如继承RetrieveUpdateAPIView
,则可以在数据接口的页面对经理人的详细信息做更新操作。
如果想要新增经理人信息,还可以进行多重继承ListCreateAPIView
。由于想要新增经理人,此处发给服务器的是POST请求,此时还需要新增加一个url映射
path('angets/', AgentView.as_view()),
但是若进行多重继承,可能会出现MRO问题method resolution order
。在继承的两个父类中,都会又get
方法,这时在服务器请求api/agents/1
想要拿到单个经理人的信息时,返回的数据依然是经理人列表。这时可以通过重写get
方法来解决这一问题,为其加上分支结构来处理。条用DEBUG模式可以发现,在RetrieveUpdateAPIView
的对象中会带有pk = {<实参>}
,所以这就是建立分支结构的依据:
def get(self, request, *args, **kwargs):
cls = RetrieveUpdateAPIView if 'pk' in kwargs else ListCreateAPIView
return cls.get(self, request, *args, **kwargs)
当我们这样修改了之后,尽管可以成功得分别请求两个数据接口的,但是新的问题又出现了。在新增经理人信息中,只有('agentid', 'name', 'tel', 'servstar')
,因为序列化器中只有这四个字段。 而在数据库中要求realstar
字段也不能为空,并且若要新增经理人,还需要为其添加其他的信息,在目前的经理人序列化器中是没有的。若此时提交POST请求,服务器就会报错。所以查看经理人的序列化器和新增经理的序列化器不能是同一个序列化器。此时新增一个添加经理人的序列化器。
class AgentCreateSerializer(serializers.ModelSerializer):
"""创建经理人"""
class Meta:
model = Agent
exclude = ('estate', )
这时还要在经理人视图函数中做一个判断,若要查询经理人列表,用AgentSimpleSerializer
,若要创建经理人或者查询经理人详细信息,用AgentCreateSerializer
def get_serializer_class(self):
return AgentCreateSerializer if self.request.method == 'POST' else AgentSimpleSerializer
若在查看经理人详情的时,想要看到经理人的所有信息,这时AgentSimpleSerializer
就不再适用了,这时还需要调整代码,创建一个可以序列化经理人完整信息的序列化器,通过fields = "__all__"来实现
。但是经理人所管理的楼盘,这样做了显示出来的只是所对应楼盘的编号,我们期望的是显示楼盘的具体名称。所以这时需要重新定义estates
这个类的序列化方法。
class AgentDetailSerializer(serializers.ModelSerializer):
"""经理人详情"""
estates = serializers.SerializerMethodField()
@staticmethod
def get_estates(agent):
queryset = agent.estates.all()[:5]
return EstateSimpleSerializer(queryset, many=True).data
class Meta:
model = Agent
fields = "__all__"
class EstateSimpleSerializer(serializers.ModelSerializer):
"""楼盘简单信息"""
class Meta:
model = Estate
fields = ('estateid', 'name')
并且修改经理人的视图函数:
class AgentView(RetrieveUpdateAPIView, ListCreateAPIView):
"""经理人视图"""
def get_queryset(self):
queryset = Agent.objects.all()
if 'pk' not in self.kwargs:
# 查询列表
queryset = queryset.only('name', 'tel', 'servstar')
else:
queryset = queryset.prefetch_related(
Prefetch('estates',
queryset=Estate.objects.all().only('name').order_by('-hot'))
)
if queryset.certificated == 1:
queryset.certificated = '真的'
return queryset
def get_serializer_class(self):
if self.request.method == 'POST':
return AgentCreateSerializer
else:
return AgentDetailSerializer if 'pk' in self.kwargs else AgentSimpleSerializer
def get(self, request, *args, **kwargs):
cls = RetrieveUpdateAPIView if 'pk' in kwargs else ListCreateAPIView
return cls.get(self, request, *args, **kwargs)
优化查询
在我们已经配置好的django-debug-toolar
侧边连中会发现,SQL语句中会出现大量的重复查询,而且是没有用的查询语句,这就使得查询变得缓慢,一旦需要查询大量的数据,这将严重拖延查询时间,这时我们需要优化SQL查询。对于一对一或者多对一,可以通过select_related()
关联对象,ORM框架就就可以生成内连接或者左外连接避免1+n
查询问题。对于多对多关联,通过prefetch_related('estates')
关联对象。
设置返回房屋信息的数据接口
配置房屋户型的数据接口(定制CRUD全套接口)
由于房屋的详细信息可以能时常发生变动,我们依然通过CBV
创建一个类来返回房屋数据接口,并继承ModelViewSet
可以进行房屋数据的增删改查操作
class HouseTypeViewSet(ModelViewSet):
queryset = HouseType.objects.all()
serializer_class = HouseTypeSerializer
但是通过ModelViewSet
做的数据接口需要在api/urls.py
中注册路由
router = DefaultRouter()
router.register('housetypes', HouseTypeViewSet)
urlpatterns += router.urls
当我们请求api/housetype
就可以返回户型的数据
并且在页面底部还可以进行增删改查操作
设置楼盘信息数据接口
在此处同经理人数据接口类似,同样分别可以查询楼盘列表和单个楼盘的详细信息。并用通过action
发出的不同请求序列化对应的不同字段,拿到我们想要的数据。这里继承了ReadOnlyModelViewSet
表示只读的一个接口。
视图函数如下:
class EstateViewSet(ReadOnlyModelViewSet):
"""楼盘视图"""
queryset = Estate.objects.all()
def get_queryset(self):
if self.action == 'list':
queryset = self.queryset.only("name")
else:
queryset = self.queryset.defer('district__parent', 'district__ishot',
'district__intro').select_related('district')
return queryset
def get_serializer_class(self):
return EstateDetailSerializer if self.action == 'retrieve' else EstateSimpleSerializer
楼盘序列化器如下:
class EstateSimpleSerializer(serializers.ModelSerializer):
"""楼盘简单序列化器"""
class Meta:
model = Estate
fields = ('estateid', 'name')
class EstateDetailSerializer(serializers.ModelSerializer):
"""楼盘详情序列化器"""
district = DistrictSimpleSerializers()
class Meta:
model = Estate
fields = '__all__'
Django框架下原生SQL查询
通过django.db.connextions['default']
拿到默认数据库的连接,返回连接代理对象,通过cursor()
拿到游标,通过execute
执行查询语句。在我的码云中有一个基于MySQL实现的python面向对象编程,可以在https://gitee.com/mysql_python中查看。
实现数据接口的分页功能
在DRF框架中,已经封装好了三种分页的类,在settings.py
文件中,在djangorestframework的配置代码这里
REST_FRAMEWORK = {
# 配置默认页面大小
'PAGE_SIZE': 5,
# 配置默认的分页类
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
}
配置好就可以在接口文档中看到已经被分页了。但是framew自带的分页功能还存在一定的瑕疵,对于CBV的数据接口,就不会进行分页,只对FBV的数据接口才显示分页。对于一些数据较少的接口,像户型的数据接口,只有8个数据,就可以不需要进行分页的功能。对于不需要分页的数据接口,只需要在视图函数这里添加pagination_class = None
就表示不需要分页器。
在浏览器的地址栏中,我们可以通过api/agents/?page=2
来跳转到第二页,但是想要通过api/agents/?size=3
来修改页面的大小则不会被执行。若希望能够执行我们期望的分页功能,这时可以自定义一个分页类,来继承PageNumberPagination
这个父类。
在api
应用中新建一个helper.py
文件来写帮助类。在PageNumberPagination
这个类中会发现page_size_query_param = None
,这就是我们需要进行修改的代码,当允许用户指定页面的大小是,我们还需要一个参数来限制最大的页面大小,在PageNumberPagination
也能找到max_page_size = None
。
from rest_framework.pagination import PageNumberPagination
class CustomPagination(PageNumberPagination):
page_size_query_param = 'size'
max_page_size = 50
写好了自定义分页器,这时需要修改配置文件,让DEFAULT_PAGINATION_CLASS
指定到我们自定义的分页类
REST_FRAMEWORK = {
# 配置默认页面大小
'PAGE_SIZE': 5,
# 配置默认的分页类
'DEFAULT_PAGINATION_CLASS': 'DEFAULT_PAGINATION_CLASS': 'api.helper.CustomPagination'
}
至此,分页功能就完成了。
但是这种分页的操作很容易暴露数据的规模,在Django中还有一种分页被称为"游标分页"。游标分页需要对数据进行排序后才能分页,这里也需要我们自定义一个游标分页类,并继承CustomPagination
类
class AgentCursorPagination(CustomPagination):
page_size_query_param = 'size'
max_page_size = 50
ordering = '-agentid'
并在经理人视图函数添上这么一句话
pagination_class = AgentCursorPagination
就能够实现游标分页功能。
这时网站数据规模的相关信息已经被改成了一窜字母。
添加Redis高速缓存(空间换时间)
缓存是典型的时间换空间策略,池化作用(线程池、连接池等)也是典型的空间换时间的策略。缓存分为声明式缓存和编程式缓存。
声明式缓存
对于咱们国家,省级行政区域基本不会变动,所以像这样数据体量不大,又不会变动,又需要经常查询到的数据,应该将其保存到缓存中,不需要每次都查询。当我们把djangodebugtoolbar
配置好后,可以在调试栏中发现,每次请求数据都需要重新查询,这样会增加数据库的压力。
这里我们为get_provinces
视图函数加上一个声明式缓存@cache_page
的装饰器。此处咱们默认使用Redis的0号服务器。
@cache_page(timeout=365 * 86400, cache='default')
将缓存存活的时间设置为一年,当我们再次请求接口,会发现在debug-toolbar
的右侧工具调试栏中,SQL
语句的查询时间已经变成了0.00ms。
为了减轻数据库的压力,我们依然要给HotCitiesView
类添加一个装饰器提供Redis高速缓存。但是cache_page
这个装饰器只能装饰函数而不能装饰类.
所以这里需要用@method_decorator(decorator=cache_page(timeout=86400, cache='default'))
,它能够将装饰函数的装饰器转换成装饰类的装饰器,由于ListAPIView
是通过get方法查询到数据,所以还需要给这个装饰器添加一个属性name=get
,此时这个装饰就就完善了。
@method_decorator(decorator=cache_page(timeout=86400, cache='default'), name='get')
当我们再次请求api/hotcities
接口时,这时已经从Redis缓存中拿到数据而不是重新查询了。
对于ModelViewSet
,增删改操作不需要假如缓存,缓存主要是要加在查询上, 不管是查单个韩式查询列表,主要是给list
和retrieve
这两个方法加装饰器。若对查询列表的装饰器加缓存可以添加属性name='list'
,若对查单个信息加缓存可以添加属性name='retrieve'
。
@method_decorator(cache_page(timeout=86400, cache='default'), name='list')
@method_decorator(cache_page(timeout=86400, cache='default'), name='retrieve')
对于其他需要加缓存的类或者函数,我们都可以采用如上的方法加入缓存,下文不再赘述。
编程式缓存
但是对于声明式缓存,非常不灵活,而且装饰器的名字很长,这时我们也可以利用编程式缓存定制相对灵活的缓存方法。这里以get_cities
视图函数为例进行讲解。
district = caches['defualt'].get(f'district: {distid}')
if district is None:
district = District.objects.filter(distid=distid).defer('parent').first()
caches['defualt'].set(f'district:{distid}', district)
这里先判断数据空中能不能拿到地区的缓存,如果有就走缓存那数据,如果没有就将数据进行缓存。但是Django封装的cache、caches只能通过set/get方法操作字符串数据类型。最直接的方法就是直接拿到redis连接,对redis进行操作,高度定制我们想要的缓存,代码如下:
@api_view(('GET', ))
def get_district(request: HttpRequest, distid) -> HttpResponse:
"""获取地区详情"""
redis_cli = get_redis_connection()
data = redis_cli.get(f'zufang:district:{distid}')
if data:
# 反序列化
district = pickle.loads(data)
else:
district = District.objects.filter(distid=distid).defer('parent').first()
data = pickle.dumps(district)
# 序列化对象到redis缓存
redis_cli.set(f'zufang:district:{distid}', data, timeout=900)
serializer = DistrictDetailSerializer(district)
return Response(serializer.data)
通过get_redis_connection()可以执行几乎所有的redis命令,但是这里需要手动序列化数据,可以利用pickle
或者是json
。
接口限流
由于咱们的api接口时开放的,所以需要对接口做一个访问限速组织用户频繁的访问接口。在DRF框架中,已经存在限流的配置,这时我们只需要将配置添加上即可。但是若要限流,必须要配置缓存才行。我们可以将IP地址作为对应缓存的键,访问次数对应值,设置访问次数的限制后,随着访问次数的增加,值减少最后归零,达到组织访问的效果。
# # djangorestframework的配置
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': (
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
),
'DEFAULT_THROTTLE_RATES': {
'anon': '30/min',
'user': '10000/day',
}
}
这里分别对匿名用户和已有用户进行限流,匿名用户每分钟最多访问30次,已有用户每天可以访问1000次。若想要自定义限流类,我们可以继承UserRateThrottle
,对其get_cache_key()
函数进行重写。
但是想针对某一接口不限流或者需要特殊的限流策略,对于CBV视图函数,可以加上throttle_classes = (A, B, C)
继承对应的自定义限流类;对于FBV视图函数,可以加上装饰器@throttle_classes(A, B, C)
添加期望的限流方法。
高级接口数据筛选
指定条件对接口进行筛选,这里以经理人的视图函数为例进行讲解。我们很有可能指定经理人的服务星级或名字经行筛选。
需要找到需要筛选的api接口的视图函数,在中间添加get_queryset(self)
的一个函数,self表示这个视图集对象,通过self.request.GET.get()
获取到请求的参数,筛选条件一般跟在请求参数的后面,让其作为条件对queryset
进行filter
操作实现对数据的筛选。
在agents
的视图函数中,添加了筛选name
和servstar
的方法,这里就有了两个filter
,这里的两个筛选条件时而且的关系,若想要做成Q对象,需要引用Q
对象:Q & Q;Q | Q;-Q。所以若在浏览器中请求agents/?name='袁'&servstar=
表示只返回名字包含“袁”的经理人,若请求api/agents/?name=袁&servstar=5
则会返回姓名包含”袁“并且服务星级是五星级的经理人的信息。
name = self.request.GET.get('name')
if name:
# where name like '<name>'
self.queryset = self.queryset.filter(name__startwith=name)
servstar = self.request.GET.get('servstar')
if servstar:
# where servstar >= 参数传过来的星级
# gte ---> great than or equal to
self.queryset = self.queryset.filter(servstar__gte=servstar)
添加索引
在通过经理人筛选的时候,我们一般会查询其姓名而不会查询其编号,但是通过查询姓名会使得数据库得查询性能变得非常糟糕,这里需要为姓名添加索引。在数据库中,可以通过如下代码建立索引:
crete index idx_agnet_name on tb_agent(ename)
当执行select * from tb_agent where name='<name>'
就不会进行全表查询,明显优化得查询性能。这种不是通过主键进行得索引查询称为非聚集查询。若名称很长,再为其添加索引会耗费空间资源,这时可以添加前缀索引:
drop index idx_agnet_name on tb_agent;
create index idx_agnet_name on tb_agent (ename(1))
此函数还对经理人的数据接口做了进一步的修改:若查询经理人的列表,通过 if self=action == list
判断,若为真就只返回经理人的姓名、电话和服务星级这三个信息,否则会返回经理人的详细信息,可以用过api/agents/1/
返回对应编号经理人的详细信息。
但是这里会遇到一个坑,经理人所对应的楼盘是一个多对多的关系,在数据库中用manytomany
表示。在视图函数中需要通过prefetch_related
来避免n+n查询问题,预抓取楼盘,并且只查询楼盘对应的名字,不需要楼盘的其他信息。
if self.action == 'list':
self.queryset = self.queryset.only('name', 'tel', 'servstar')
else:
# 避免1 + n查询
# 一对一 / 多对一关联: select_related
# 多对多关联: prefetch_related
self.queryset = self.queryset.prefetch_related(
Prefetch('estates',
queryset=Estate.objects.all().only('name').order_by('-hot'))
)
这里还需要对AgentViewSet
添加一个方法get_serializer_class
,表示若查询经理人详情则通过AgentDetailSerializer
来序列化字段,若查询经理人列表就通过AgentSimpleSerializer
来学列话字段。
def get_serializer_class(self):
if self.action in ('create', 'update'):
return AgentCreateSerializer
return AgentDetailSerializer if self.action == 'retrieve' else AgentSimpleSerializer
利用三方库做接口数据筛选
参考楼盘视图集的视图函数:
django-filter
可以配合djangorestframework
做接口数据筛选
首先需要在izufang/settings.py
中加上配置文件
INSTALLED_APPS = [
# 添加django-filter,这里加应用的时候,是“filters”而不是“filter”
'django_filters',
]
在视图函数中添加
filter_backends = (DjangoFilterBackend, )
filter_fields = ('name', 'hot', 'district')
可以进行精确查询的,但是这样不够灵活。在djangi_filters
中有一个DjangoFilterBackend
类专门做接口数据筛选,而OrderingFilter
是支持排序的一个类。通过django-filter
可以做指定条件的筛选,但是都只能做精确筛选,我们可以自定义一个查询方法来实现模糊查询。在helper.py
文件中创建一个EstateFilterSet
自定义筛选器
class EstateFilterSet(filterset.FilterSet):
"""自定义楼盘筛选器"""
# filter(name__contains='<name>')
name = filterset.CharFilter(lookup_expr='contains')
# filter(hot__gte=minhot, hot__lte=maxhot)
minhot = filterset.NumberFilter(field_name='hot', lookup_expr='gte')
maxhot = filterset.NumberFilter(field_name='hot', lookup_expr='lte')
dist = filterset.NumberFilter(field_name='district')
class Meta:
model = Estate
fields = ('name', 'minhot', 'maxhot', 'dist')
随后在视图函数中添加方法:
filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_class = EstateFilterSet
# 默认热度作为排序字段
ordering = '-hot'
# 其他排序可选字段
ordering_fields = ('district', 'hot', 'name')
【说明】
- 如果楼盘的名称需要做模糊查询,通过筛选字典的lookup_expr属性指定是精确还是模糊查询,可以使用
exact/contains/startwith/endwith
来表示方法。 - 如果需要根据楼盘热度范围来筛选,
minhot
和maxhot
都对应了hot
属性,但是需要用lookup_expr
指定gt/gte/lt/lte
。 - 可以修改查询键的名称,可以通过
field_name
指定到原始的名称。
项目和项目源代码持续更新中,代码请参考我的码云:https://gitee.com/dcstempt_ping/izufang_rent
谢谢阅读❤