Django过滤和分页
场景描述
在编写WEB应用时,会通过类似www.example.com/books/
的URL查询书本列表。如果要查询作者John的书籍呢,只要在URL后加过滤参数就好,例如www.example.com/books/?author=John
。
下面我们来看看Django的实现:
定义一个书本的模型,包含书本名、作者和价格。在后台不指定条件的情况下,返回所有的书籍,指定作者名,就返回该作者名下的书籍。
# models.py
class Book(models.Model):
name = models.CharField('名称', max_length=128)
author = models.CharField('作者', max_length=32)
price = models.DecimalField('价格', decimal_places=2, max_digits=7)
created_date = models.DateTimeField('添加时间', auto_now_add=True)
# views.py
from .models import Book
import json
import decimal
from datetime import datetime
from django.http import HttpResponse
class CustomEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, decimal.Decimal):
return str(o)
if isinstance(o, datetime):
return o.strftime('%Y-%m-%d %H:%M:%S')
def get_books(request):
author = request.GET.get('author', '')
if author: # 指定作者
books = Book.objects.filter(author=author).values('name', 'author', 'price', 'created_date')
else:
books = Book.objects.all().values('name', 'author', 'price', 'created_date')
ret = {
"total": books.count(),
"data": list(books)
}
ret = json.dumps(ret, cls=CustomEncoder)
return HttpResponse(ret)
#urls.py
from goods import views as goods_view
urlpatterns = [
url('^books/', goods_view.get_books)
]
现在让我们通过浏览器查看效果:
在浏览器输入127.0.0.1:8000/books/
返回的是提前在数据库内存储的四条记录,加了过滤参数author
,返回的是作者John名下的两本书籍,是不是很简单呢。
多个参数查询
前面过滤只有一个author字段,如果有四五个参数呢,代码就会像下面一样。虽然可以实现需要的功能,但是重复的代码比较多,使用django-filter
就可以简化这些操作。
def get_books(request):
author = request.GET.get('author', '')
created_date__gt = request.GET.get('created_date__gt', '')
name = request.GET.get('name', '')
books = Book.objects.all().values('name', 'author', 'price', 'created_date')
if author: # 指定作者
books = books.filter(author=author)
if created_date__gt: # 录入时间大于created_date__gt
books = books.filter(created_date__gt=created_date__gt)
if name: # 指定书名
books = books.filter(name=name)
ret = {
"total": books.count(),
"data": list(books)
}
ret = json.dumps(ret, cls=CustomEncoder) # 序列化decimal和时间
return HttpResponse(ret)
django-filter介绍
Django-filter is a generic, reusable application to alleviate writing some of the more mundane bits of view code. Specifically, it allows users to filter down a queryset based on a model’s fields, displaying the form to let them do this.
django-filter
一般与django的第三方框架restful framework
组合使用, 我也是在使用
restful的时候,才想到能不能单独拿出来使用。详细了解可以查询官方文档
下面是演示代码,CustomFilter
继承FilterSet
,使用方式类似于Django的ModelForm,在Meta中指定模型,fields则指定过滤的条件。exact
指明字段相等,lt
,gt
则会与字段名组合成created_date__gt
和created_date__lt
在filter()中使用,是不是很方便。django-filter其它功能和使用方式,在这里就不详细叙述了,感兴趣的同学自己探索使用吧。
import django_filters
class CustomFilter(django_filters.FilterSet):
class Meta:
model = Book
fields = {
'name': ['exact'],
'created_date': ['lt', 'gt', 'year'],
'author': ['exact'],
'price': ['lt', 'gt']
}
def get_books(request):
books = Book.objects.all().values('name', 'author', 'price', 'created_date')
f = CustomFilter(request.GET, queryset=books)
ret = {
"total": f.qs.count(),
"data": list(f.qs)
}
ret = json.dumps(ret, cls=CustomEncoder)
return HttpResponse(ret)
Django分页
就像上面图片展示的一样,浏览器请求127.0.0.1:8000/books/
,服务器返回json格式的书本列表。目前数据库内只有四个记录,如果是10000条记录,服务端会返回10000条数据么? 服务端肯定是不会直接返回这么多条数的,怎么办呢,这个时候分页就起到作用了。类似请求如下127.0.0.1:8000/books/?page_size=1&page_num=5
,假如有10000条记录,服务端根据page_size分成2000页,每页5个。终端只要控制具体显示哪一页的记录就好,人也看不过来那么多数据。
Django自带有分页器类Paginator
,可以很方便的实现分页功能。代码如下:
from django.core.paginator import Paginator
def get_books(request):
page_size = request.GET.get('page_size', 4)
page_num = request.GET.get('page_num', 1)
books = Book.objects.all().values('name', 'author', 'price', 'created_date')
f = CustomFilter(request.GET, queryset=books)
pagination = Paginator(list(f.qs), page_size)
_list = pagination.page(page_num)
ret = {
"total": pagination.count,
"data": _list.object_list
}
ret = json.dumps(ret, cls=CustomEncoder)
return HttpResponse(ret)
下图显示了代码运行效果,总共四条数据,现在显示了其中两条。
因为每次都要写上request.GET.get('page_size', 4)
这样的代码,觉得繁琐,就把django-restful-framework的相关分页代码抄过来,拿来使用
class CustomPaginator(object):
page_size_query_param = 'page_size'
page_size = 4
page_num = 1
page_query_param = 'page_num'
last_page_strings = ('last',)
max_page_size = 10
def paginate_queryset(self, queryset, request):
page_size = self.get_page_size(request)
if not page_size:
return None
paginator = Paginator(queryset, page_size)
page_number = request.GET.get(self.page_query_param, 1)
if page_number in self.last_page_strings:
page_number = paginator.num_pages
try:
self.page = paginator.page(page_number)
except InvalidPage as exc:
return None
return list(self.page)
def get_page_size(self, request):
if self.page_size_query_param:
try:
return self._positive_int(request.GET[self.page_size_query_param],
strict=True,
cutoff=self.max_page_size)
except (KeyError, ValueError):
pass
return self.page_size
def get_paginated_response(self):
data = {
'total': self.page.paginator.count,
'rows': self.page.object_list
}
ret = json.dumps(data, cls=CustomEncoder)
return HttpResponse(ret)
@staticmethod
def _positive_int(integer_string, strict=False, cutoff=None):
"""
Cast a string to a strictly positive integer.
"""
ret = int(integer_string)
if ret < 0 or (ret == 0 and strict):
raise ValueError()
if cutoff:
return min(ret, cutoff)
return ret
def get_books(request):
# 过滤条件
books = Book.objects.all().values('name', 'author', 'price', 'created_date')
f = CustomFilter(request.GET, queryset=books)
# 分页
pagination = CustomPaginator()
pagination.paginate_queryset(f.qs, request)
response = pagination.get_paginated_response()
return response
这样一个代码比较少的分页和过滤功能就实现了。