简介
本文主要整理了Django支持的多种复杂条件筛选数据库数据集的方法,同时属于上篇推文的拓展版,感兴趣的大兄弟们可以戳进去看看:django多种查询筛选数据库方式_Sean_TS_Wang的博客-优快云博客
目录
三、rest-framework提供的SearchFilter
正文
本质上来说所有的多条件筛选方式最终都会转化成SQL语句进行操作,但是Django提供了多种方式,简化对数据库筛选操作。
一、使用原生SQL实现多条件筛选(extra)
上篇推文提到几种方式使用原生SQL操作数据库,这次使用extra做例子讲解,实际上效果是差不多的。
from django.db import models
class user(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=5)
age = models.IntegerField()
class Meta:
db_table='user'
class animal(model.Model):
kind = models.CharField(primary_key=True, max_length=5)
name = models.CharField(max_length=5)
sex = models.BooleanField()
class Meta:
db_table='animal'
# select user.id,user.name,user.age from user, animal where user.name=animal and user.age>18
User = User.objects.all().extra(tables=['animal'],
where=[
'user.name = animal.name',
'user.age > 18'
]
)
优点:支持多表筛选,同时更符合数据库查询,甚至可以使用数据库函数简化查询
缺点:老问题,SQL注入。如何防止SQL注入的方法在上篇推文里面有写,基本思路就是对传过来的参数不要直接拼接到SQL语句中 ,而是先经过自己的排序处理再安装在相应的位置。
二、Django ORM 实现多条件筛选
简单筛选
基本筛选
from django.db import models
from django.core import serializers
from django.Response import JSONResponse
class user(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=5)
age = models.IntegerField()
class Meta:
db_table='user'
def userView(request):
queryset = user.objects.all()
id = request.GET.get('id',None)
name = request.GET.get('name',None)
age = request.GET.get('age',None)
if id:
queryset = queryset.filter(id=id)
if name:
queryset = queryset.filter(name=name)
if age:
queryset = queryset.filter(age=age)
queryset = serializers.serializer('json',queryset)
return JSONResponse({'data':queryset})
这样每个字段都需要if判断一下,有些麻烦,可以写个循环省略一下,因此Django也提供了一种批量参数筛选的方式——字典筛选(SearchFilter)
def userView(request):
queryset = user.objects.all()
filter_fields = {}
for field in user._meta.fields:
param = request.GET.get(field.name, None)
if param:
filter_fields[field.name] = param
queryset = queryset.filter(**filter_fields)
queryset = serializers.serilizer('json',queryset)
return JSONResponse({'data':queryset})
优点:使用简便,支持大部分的筛选要求
缺点:所有查询条件都是并列的,相当于SQL语句中的AND,想要实现其他查询方式,需要额外设置查询条件。
Q方法
针对上述简答筛选的缺点,Django提供了Q方法,使得查询条件变得更加灵活。下列方法添加了同一个字段的OR筛选,使用逗号隔开
from django.db.models import Q
from functools import reduce
def userView(request):
queryset = user.objects.all()
for field in user._meta.fields:
params = request.GET.get(field.name, None)
if params:
params = params.split(',')
filter_fields = [Q(**{field.name: param}) for param in params]
filter_field = reduce(lambda x,y:x|y, filter_fields)
queryset = queryset.filter(filter_field)
queryset = serializers.serializer('json',queryset)
return JSONResponse({'data':queryset})
优点:针对简单查询进行优化,可以对查询条件同时做与或非操作(简单查询只能同时做与和非操作)
缺点:暂时还没发现,好像是不能跨表查询,没试过跨表查询,所以不清楚,有大佬可以一起讨论一下
三、rest-framework提供的SearchFilter
上述两种方法都还是需要在视图层(view),也就是业务流中编辑相应的筛选条件,导致视图层的业务逻辑十分繁琐。于是,勤劳的程序员们就将功能重复的部分都拆分了出来,分割为各个不同的部分,例如筛选器,序列化器,认证,分页等等。
当然拆分不是重点,重点是,拆分完之后,我们就可以直接使用其他人封装好的部分去使用。传说中的拿来主义,我们不生成代码,我们只是代码的搬运工(农夫山泉广告,滑稽脸)
SearchFilter使用
from rest_framework.generics import ListAPIView
from rest_framework import serializers
from rest_framework.filters import SearchFilter
class user(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=5)
age = models.IntegerField()
class Meta:
db_table='user'
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = user
fields = '__all__'
class UserList(ListAPIView):
queryset = user.objects.all()
serializer_class = UserSerializer
filter_class = (SearchFilter,)
search_fields = ('name', )
使用方法很简单,只需要在类视图中设置相应的筛选器,以及需要筛选的字段。但是这功能是怎么实现的呢? 思路其实很简单,将每个需要筛选的字段重组成Q方法包裹的筛选条件,从而到达筛选的效果。
关键源码分析
# 将每个要查询的字段变成field__icontains
def construct_search(self, field_name):
lookup = self.lookup_prefixes.get(field_name[0])
if lookup:
field_name = field_name[1:]
else:
lookup = 'icontains'
# LOOKUP_SEP = '__'
return LOOKUP_SEP.join([field_name, lookup])
# 开始组合查询语句
def filter_queryset(self, request, queryset, view):
# 获取需要查询的字段,通过view中的search_fields字段设置,源码没有用到request,但是备注说了可以通过子类重写这个方法,动态设置查询字段
search_fields = self.get_search_fields(view, request)
# 获取前端传过来的参数
search_terms = self.get_search_terms(request)
if not search_fields or not search_terms:
return queryset
# 关键代码一,变成双下划线筛选方式
orm_lookups = [
self.construct_search(str(search_field))
for search_field in search_fields
]
base = queryset
conditions = []
# 关键代码二,变成Q筛选条件,如果同一个字段有两个或以上的参数,就用or
for search_term in search_terms:
queries = [
models.Q(**{orm_lookup: search_term})
for orm_lookup in orm_lookups
]
conditions.append(reduce(operator.or_, queries))
queryset = queryset.filter(reduce(operator.and_, conditions))
if self.must_call_distinct(queryset, search_fields):
# Filtering against a many-to-many field requires us to
# call queryset.distinct() in order to avoid duplicate items
# in the resulting queryset.
# We try to avoid this if possible, for performance reasons.
queryset = distinct(queryset, base)
return queryset
优点:将筛选器模块化,变成可插拔
缺点:筛选功能简单,而且只能支持字符串类型的筛选,像时间之类的筛选都不行。原因是在源码中只设置了支持四种筛选条件,加上默认的icontains 一共五种筛选条件,而且其中有一种search(全文检索)只支持MySQL数据库。因此这个方法只是用来抛砖引玉或者是给有志向自己造轮子的大兄弟提供一个可参考思路的。
四、django-filter
重头戏来了,上个筛选方式SearchFilter说的引玉说的就是这个。在上一篇推文中,只是简单地用函数视图调用了一下这个筛选器,并没有深入的挖掘它的价值。这次,我们来看看这个筛选器能做到什么地步。
这次我们使用rest-framework的类视图简化一下视图层的编辑。用法和SearchFilter相似,就是DjangoFilterBackend过滤器要在settings的REST_FRAMEWORK字段里面设置一下,不然会报错。
基本查询
from django.rest_framework import DjangoFilterBackend
from django_filter import FilterSet
class UserFilter(FilterSet):
class Meta:
model = user
fields = ['name']
class UserList(ListAPIView):
queryset = user.objects.all()
serializer_class = UserSerializer
filter_Backend = (DjangoFilterBackend,)
filterset_class = UserFilter
filterset_fields = ('name', )
上面是一个最基本的使用方法,下面我们来看看django-filter是如何实现其他复杂查询的。
对了,顺便说一下,新版django-filter将filter_class和filter_fields改成了filterset_classs和filterset_fields,但是还是相互兼容的。
进阶查询
from django_filter import FilterSet
from django_filter.rest_framework import NumberFilter, CharFilter
class animal(model.Model):
kind = models.CharField(primary_key=True, max_length=5)
name = models.CharField(max_length=5)
sex = models.BooleanField()
class Meta:
db_table='animal'
class UserFilter(FilterSet):
age_adult = NumberFilter(field_name='age', lookup_expr='gte')
name_similar_pet = CharFilter(field='name', method='find_pet_by_name')
class Meta:
model = user
fields = ['name']
def find_pet_by_name(self,queryset,name,value):
return queryset.extra(table=['animal'],where=['user.name=animal.name','user.name=%s'],params=[value])
django-filter 支持Django的双下划线查询,只需要给对应的字段设置lookup_expr值就好了,例子只是其中一种写法,还有其他的写法感兴趣的小伙伴可以去官网查看。(例如键值对写法以及在键值对内嵌lamda函数)
同时django-filter还提供了函数的方法来支持一些复杂的SQL语句查询。
那么Django-filter又是怎么将这些操作转化成类的对象进行操作的呢?下面我们来一下相关源码。
关键源码分析
class FilterSet(filterset.FilterSet)
我们继承的FilterSet类,主要是实现表单数据上传和页面展示的,并没有涉及到数据库的字段查询,真正实现字段查询的是FilterSet类的父类——filterset.FilterSet
class FilterSet(BaseFilterSet, metaclass=FilterSetMetaclass):
pass
我们可以看到这个FilterSet 类啥事都没干,光继承了两个类,合着这是在套娃呢?
首先我们先来看看metaclass=FilterSetMetaclass这个是个啥东东
class FilterSetMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['declared_filters'] = cls.get_declared_filters(bases, attrs)
new_class = super().__new__(cls, name, bases, attrs)
new_class._meta = FilterSetOptions(getattr(new_class, 'Meta', None))
new_class.base_filters = new_class.get_filters()
# TODO: remove assertion in 2.1
assert not hasattr(new_class, 'filter_for_reverse_field'), (
"`%(cls)s.filter_for_reverse_field` has been removed. "
"`%(cls)s.filter_for_field` now generates filters for reverse fields. "
"See: https://django-filter.readthedocs.io/en/master/guide/migration.html"
% {'cls': new_class.__name__}
)
return new_class
@classmethod
def get_declared_filters(cls, bases, attrs):
filters = [
(filter_name, attrs.pop(filter_name))
for filter_name, obj in list(attrs.items())
if isinstance(obj, Filter)
]
# Default the `filter.field_name` to the attribute name on the filterset
for filter_name, f in filters:
if getattr(f, 'field_name', None) is None:
f.field_name = filter_name
filters.sort(key=lambda x: x[1].creation_counter)
# merge declared filters from base classes
for base in reversed(bases):
if hasattr(base, 'declared_filters'):
filters = [
(name, f) for name, f
in base.declared_filters.items()
if name not in attrs
] + filters
return OrderedDict(filters)
我们先来说说metaclass是个啥,Python有句名言,叫做万物皆对象。也就是说类,也是一个对象。那类属于谁的对象呢?
元类,我们可以看到FilterSetMetaclass继承了type,type就是一个元类,没错就是平常我们使用的type(),我们class一个新的类,本质上其实就是使用type()去创建一个类实例。而metaclass就是继承了type的一个子类,重写了__new__()方法。重新实现了一个类的构造方式。下面我们逐步讲解在FilterSetMetaclass被构造的时候,做了一些什么事情
一、首先重写了__new__()方法,那我们来看看,传入的值分别是什么:
name:UserFilter
bases:(<class 'django_filters.rest_framework.filterset.FilterSet'>,)
attrs:{
'__module__': 'website.filter',
'__qualname__': 'UserFilter',
'Meta': <class 'website.filter.UserFilter.Meta'>,
'declared_filters': OrderedDict([
('age_adult', <website.filter.NumberFilter object at 0x0000021888314F88>),
('name_similar_pet', <website.filter.CharFilter object at 0x0000021888314F88>)
])
}
可以看出传过来的分别代表着这个要构造的类的名字,它的父类,以及它的相关属性。
二、接着,attrs['declared_filters'] = cls.get_declared_filters(bases, attrs),这个方法是将传过来的attrs从中获取需要筛选的字段打包成元祖列表,变成下面这样:
[
('age_adult', <django_filters.filters.NumberFilter object at 0x000001EB147C51C8>),
('name_similar_pet', <django_filters.filters.CHarFilter object at 0x000001EB147C50C8>),
]
三、然后调用父类的__new__()构造类,接着用FilterSetOptions(getattr(new_class, 'Meta', None))方法获取新构建的类中的class Meta里面的对应值,例如model,筛选字段啥的。
四、紧接着,重点来了get_filters()这个是调用BaseFilterSet这个父类的类方法去做筛选条件判断,内部逻辑如下:
- 比对需要筛选的字段是否符合model的定义,get_model_field(cls._meta.model, field_name)
- 将符合的筛选字段转化成合适的形式,cls.filter_for_field(field, field_name, lookup_expr),内部使用了Django的开放自定义Lookup API(下一节的内容)
@classmethod
def get_filters(cls):
"""
Get all filters for the filterset. This is the combination of declared and
generated filters.
"""
# No model specified - skip filter generation
if not cls._meta.model:
return cls.declared_filters.copy()
# Determine the filters that should be included on the filterset.
filters = OrderedDict()
fields = cls.get_fields()
undefined = []
for field_name, lookups in fields.items():
field = get_model_field(cls._meta.model, field_name)
# warn if the field doesn't exist.
if field is None:
undefined.append(field_name)
for lookup_expr in lookups:
filter_name = cls.get_filter_name(field_name, lookup_expr)
# If the filter is explicitly declared on the class, skip generation
if filter_name in cls.declared_filters:
filters[filter_name] = cls.declared_filters[filter_name]
continue
if field is not None:
filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr)
# filter out declared filters
undefined = [f for f in undefined if f not in cls.declared_filters]
if undefined:
raise TypeError(
"'Meta.fields' contains fields that are not defined on this FilterSet: "
"%s" % ', '.join(undefined)
)
# Add in declared filters. This is necessary since we don't enforce adding
# declared filters to the 'Meta.fields' option
filters.update(cls.declared_filters)
return filters
我们可以看出,经过上面四步 FilterSetMetaclass类除了完成基本功能对类的构造以外,同时还完成了对需要筛选字段的转化和校验。
因此我们也可以判断出,继承的另一个类BaseFilterSet,才是真正完成对数据库筛选操作的主体。源码太多,我只贴出具体筛选操作的源码进行讲解
def filter_queryset(self, queryset):
"""
Filter the queryset with the underlying form's `cleaned_data`. You must
call `is_valid()` or `errors` before calling this method.
This method should be overridden if additional filtering needs to be
applied to the queryset before it is cached.
"""
for name, value in self.form.cleaned_data.items():
queryset = self.filters[name].filter(queryset, value)
assert isinstance(queryset, models.QuerySet), \
"Expected '%s.%s' to return a QuerySet, but got a %s instead." \
% (type(self).__name__, name, type(queryset).__name__)
return queryset
可以很清晰的看出,在一个循环内对表单数据进行在筛选(表单数据哪里来的?还记得开头说的FilterSet类吗?)其中self.filters就是FilterSetMetaclass中get_filters()设置的。
大致上django-filter的内部实现分为三个部分:
- 表单数据的上传和页面展示(FilterSet)
- 筛选字段的校验和转化(metaclass=FilterSetMetaclass)在类构造过程中完成
- 具体实现数据库筛选(BaseFilterSet)
下面是django-filter对mode字段的支持筛选字段,可别用错咯
FILTER_FOR_DBFIELD_DEFAULTS = {
models.AutoField: {'filter_class': NumberFilter},
models.CharField: {'filter_class': CharFilter},
models.TextField: {'filter_class': CharFilter},
models.BooleanField: {'filter_class': BooleanFilter},
models.DateField: {'filter_class': DateFilter},
models.DateTimeField: {'filter_class': DateTimeFilter},
models.TimeField: {'filter_class': TimeFilter},
models.DurationField: {'filter_class': DurationFilter},
models.DecimalField: {'filter_class': NumberFilter},
models.SmallIntegerField: {'filter_class': NumberFilter},
models.IntegerField: {'filter_class': NumberFilter},
models.PositiveIntegerField: {'filter_class': NumberFilter},
models.PositiveSmallIntegerField: {'filter_class': NumberFilter},
models.FloatField: {'filter_class': NumberFilter},
models.NullBooleanField: {'filter_class': BooleanFilter},
models.SlugField: {'filter_class': CharFilter},
models.EmailField: {'filter_class': CharFilter},
models.FilePathField: {'filter_class': CharFilter},
models.URLField: {'filter_class': CharFilter},
models.GenericIPAddressField: {'filter_class': CharFilter},
models.CommaSeparatedIntegerField: {'filter_class': CharFilter},
models.UUIDField: {'filter_class': UUIDFilter},
# Forward relationships
models.OneToOneField: {
'filter_class': ModelChoiceFilter,
'extra': lambda f: {
'queryset': remote_queryset(f),
'to_field_name': f.remote_field.field_name,
'null_label': settings.NULL_CHOICE_LABEL if f.null else None,
}
},
models.ForeignKey: {
'filter_class': ModelChoiceFilter,
'extra': lambda f: {
'queryset': remote_queryset(f),
'to_field_name': f.remote_field.field_name,
'null_label': settings.NULL_CHOICE_LABEL if f.null else None,
}
},
models.ManyToManyField: {
'filter_class': ModelMultipleChoiceFilter,
'extra': lambda f: {
'queryset': remote_queryset(f),
}
},
# Reverse relationships
OneToOneRel: {
'filter_class': ModelChoiceFilter,
'extra': lambda f: {
'queryset': remote_queryset(f),
'null_label': settings.NULL_CHOICE_LABEL if f.null else None,
}
},
ManyToOneRel: {
'filter_class': ModelMultipleChoiceFilter,
'extra': lambda f: {
'queryset': remote_queryset(f),
}
},
ManyToManyRel: {
'filter_class': ModelMultipleChoiceFilter,
'extra': lambda f: {
'queryset': remote_queryset(f),
}
},
}
优点:针对SearchFilter来说,在其基础之上,功能更加强大,完美兼容django锁支持的筛选方式,
缺点:暂时没发现(可能是我太菜了,欢迎各位大佬留言)
五、自定义筛选方式
官方链接:自定义查询器 | Django 文档 | Django
如果上述的筛选方式您觉得还是不够用的话,那么django也支持你自定义筛选器,这就是运轮子和造轮子的区别,当然你想直接砍树获取造轮子的木材的话,建议自己开发一个ORM系统。
为了写这篇推文,专门把官方文档看了一遍,但是自己没有实际运用过,毕竟只是个卑微的搬运工有问题的话欢迎各位大佬指出。话不多说,进入正题。
基本筛选器
自定义的筛选器都是通过继承Lookup实现的,通过重写他的as_sql()方法,达到自定义筛选条件。
但是光实现自定义筛选器类还不行,还需要注册筛选器,就像光实现了view是无法使用的,需要注册到urls中一样的道理
from django.db.models import Lookup, Field
@Field.register_lookup
class NotEqual(Lookup):
lookup_name = 'ne'
def as_sql(self, compiler, connection):
lhs, lhs_params = self.process_lhs(compiler, connection)
rhs, rhs_params = self,process_rhs(compiler, connection)
params = lhs_params + rhs_params
return '%s <> %s' % (lhs,rhs), params
转化器
上面那个只是针对通用情况的自定义筛选器。如果你想对某个字段做特定筛选的话,就需要用到转换器。(例如postgresql一些字段有许多函数可以使用)
from django.db.models import Transform, Field
@Field.register_lookup
class NotEqual(Transform):
lookup_name = 'abs'
function = 'ABS'
这部分就简单演示到这(毕竟也没有实际用过,不好误人子弟,就搬了两个官网的小demo,太多了拍被说水字数)
优点:支持数据库的函数筛选,虽然对字段进行函数操作会降低查询的性能,但是和数据库的兼容性变得更好了呢
缺点:费事,如果不是极端情况或者想要自己二次开发Django框架的大兄弟还不不用这么费事了吧。
总结
以上就是就是多条件情况下查询数据库的多种方式(感觉自定义筛选器不算多条件,应该归类到上一篇推文多种方式查询数据库才对)。目前django-filer和extra结合使用,感觉满足大部分的业务需求(别打脸)。如果有更多其他的多条件查询方式,欢迎各位大佬留言讨论。