1、为什么会有自定义QuerySet?
这里举一个我们公司出现的情景,我们公司最新增加了广告投放,因此需要基于广告投放做一个分析,广告投放有5个utm参数,不知道可以自己去百度。基于广告分析有三张表,假定分别为ABC,这ABC三张表均有这5个utm参数。现在有一个统计分析的任务,需要对这三张表进行utm参数的过滤,这种过滤可以时 and 过滤,也可以是 or 过滤。下面以 and 过滤进行讲解。
class AdverIcgooLog(models.Model):
ymd = models.CharField(max_length=8, null=True, help_text="访问日期")
......
utm_source = models.CharField(max_length=100, null=True, help_text="url中的同名参数值")
utm_medium = models.CharField(max_length=100, null=True, help_text="url中的同名参数值")
utm_campaign = models.CharField(max_length=100, null=True, help_text="url中的同名参数值")
utm_term = models.CharField(max_length=100, null=True, help_text="url中的同名参数值")
utm_content = models.CharField(max_length=100, null=True, help_text="url中的同名参数值")
......
自定义QuerySet
第一个例子
需求:前端查询的时候,希望可以对 utm_source 进行or过滤,前端传递几个 utm_source的值,后端就拿出包含这几个utm_source值的数据返回给前端。因为 or 是需要Q函数的,这种特定的需求需要专门写 orm,假如有三个查询接口都有如此操作,那么这个这部分 or 过滤代码就需要写三遍,这三个代码可说几乎一摸一样,那么我们可以将其抽取出来。这里就需要自定义 QuerySet
自定义QuerySet
class AdverQuerySet(models.QuerySet):
def filter_utms(self, **kwargs):
utms = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']
qs = self
q = Q()
q.connector = 'and'
for x in utms:
utm = kwargs.get(x, None)
if utm == ['']:
utm = []
if utm:
q.children.append(('{}__in'.format(x), utm))
return qs.filter(q)
使用自定义QuerySet
class AdverIcgooLog(models.Model):
"""
此表是一个分区表,只存放icgoo中的推广日志,且完成字段的过滤以及转换
"""
ymd = models.CharField(max_length=8, null=True, help_text="访问日期")
......
utm_source = models.CharField(max_length=100, null=True, help_text="url中的同名参数值")
utm_medium = models.CharField(max_length=100, null=True, help_text="url中的同名参数值")
utm_campaign = models.CharField(max_length=100, null=True, help_text="url中的同名参数值")
utm_term = models.CharField(max_length=100, null=True, help_text="url中的同名参数值")
utm_content = models.CharField(max_length=100, null=True, help_text="url中的同名参数值")
......
objects = AdverQuerySet.as_manager()
class Meta:
db_table = 'activity_adver_icgoo_log'
查询和使用
class Command(BaseCommand):
def handle(self, *args, **options):
kwargs = dict(utm_source=['baidupc', 'baidumb'])
res = AdverIcgooLog.objects.filter_utms(**kwargs).filter(ymd='20220818')[:100].values()
for item in res:
print(item)
再来一个例子
比如我们做数据分析,经常结合日期来,前端传递的日期是一个数组,因此可以自定义一个依据日期完成过滤的filter, 这样就可以简化很多代码
自定义 QuerySet
class AdverQuerySet(models.QuerySet):
......
def filter_date(self, date, default=True):
if date == ['']:
date = []
if date:
ymd1 = date[0].replace('-', '')
ymd2 = date[1].replace('-', '')
elif default:
ymd1 = (datetime.date.today() + datetime.timedelta(days=-1)).strftime('%Y%m%d')
ymd2 = ymd1
else:
return self
return self.filter(ymd__range=(ymd1, ymd2))
使用QuerySet过滤
class Command(BaseCommand):
def handle(self, *args, **options):
date = ['2022-12-18', '2022-12-19']
res = AdverIcgooLog.objects.filter_date(date)[:100].values('id', 'adver_url')
for item in res:
print(item)
结果:
源码探究
在Django中每一个Model都有一个默认的 objects,如果我们没有自定义的话,objects有许多方法,例如 filter, count等等,但是 objects 是一个 Manager 对象,点进 Manager 查看不到 filter 等方法,这些方法本来是QuerySet所有,Manager 怎么可以调用?
可以看到 Manager 继承的是 BaseManager 的 from_queryset 方法返回的一个对象,注意 python 的 type 方法如果只传递了一个值,则返回该值的类型,如果传递的是三个参数,则尝试动态生成一个类对象。第一个参数是新的类名,第二个参数是一个元组,是新的类对象将要继承的类,第三个参数是一个字典,存放的是类属性。
接下来我们需要对应一下参数:
由于Manager继承的时候是 BaseManager调用的 from_queryset, 因此这个 cls 就是 BaseManager 本身, queryset_class 则是 QuerySet 这个类对象,此时 class_name 为None。因此经过计算 class_name 最终的值为 BaseManagerFromQuerySet, class_name 传递给了 type 作为新的类名,同时洗呢类对象继承了 BaseManager, 又通过 _get_queryset_methods 方法将 QuerySet 类的方法都拉取出来赋给了 BaseManagerFromQuerySet 作为类方法。而由于 Manager 继承了 BaseManagerFromQuerySet,因此也就有了 QuerySet 的方法。
在本文最开始的时候,我做的是自定义 QuerySet,通过调用 as_manager 方法,最后竟然也得到了一个 Manager 的对象,请看源码
可以看到最后也还是 Manager 调用了 from_queryset 方法,按照前面所讲的,我们最终得到的类对象应该叫做 ManagerFromAdverQuerySet,下面看看是不是
下面的代码可以查看某个类的继承顺序:
可以得到结论,无论是自定义 QuerySet 还是自定义 Manager 没有区别
类属性拷贝方法 BaseManager._get_queryset_methods
在前面的演示以及代码截图中,我们可以注意到一个非常关键的方法 _get_queryset_methods,顾名思义,获取 QuerySet 方法的方法,正是他获取了 QuerySet 或者是自定义的 QuerySet 的方法,并放入了动态生成的新Manager 类中
下面具体探究一下这个方法内部的每一步,到底做了什么,这个方法如何实现的
首先,在源码文件里修改代码,实际运行时是不生效的。因此我们可以将这一块儿的代码复制出来,单独运行一下试试。下面是经过删减,保留核心有用的。
class BaseManager:
@classmethod
def _get_queryset_methods(cls, queryset_class):
def create_method(name, method):
def manager_method(self, *args, **kwargs):
print(self.__class__)
return getattr(self.get_queryset(), name)(*args, **kwargs)
manager_method.__name__ = method.__name__
manager_method.__doc__ = method.__doc__
return manager_method
new_methods = {}
for name, method in inspect.getmembers(queryset_class, predicate=inspect.isfunction):
# Only copy missing methods.
if hasattr(cls, name):
continue
queryset_only = getattr(method, 'queryset_only', None)
if queryset_only or (queryset_only is None and name.startswith('_')):
continue
new_methods[name] = create_method(name, method)
return new_methods
@classmethod
def from_queryset(cls, queryset_class, class_name=None):
if class_name is None:
class_name = '%sFrom%s' % (cls.__name__, queryset_class.__name__)
return type(class_name, (cls,), {
'_queryset_class': queryset_class,
**cls._get_queryset_methods(queryset_class),
})
def get_queryset(self):
"""
Return a new QuerySet object. Subclasses can override this method to
customize the behavior of the Manager.
"""
return self._queryset_class(model=self.model, using=self._db, hints=self._hints)
以上的三个方法是最重要的。我们一个一个拆解。
首先看 _get_queryset_methods 方法
_get_queryset_methods 大致看上去,主要有两部分组成,一个是内部类,一个是提取QuerySet方法的循环代码。内部类比较难以理解,先看提取方法的循环代码。
new_methods = {}
for name, method in inspect.getmembers(queryset_class, predicate=inspect.isfunction):
# Only copy missing methods.
if hasattr(cls, name):
continue
queryset_only = getattr(method, 'queryset_only', None)
if queryset_only or (queryset_only is None and name.startswith('_')):
continue
new_methods[name] = create_method(name, method)
inspect.getmembers(queryset_class, predicate=inspect.isfunction)
是一个Python环境所有的代码,inspect.isfunction 表示提取的是方法,queryset_class 正是最开始传递进来的参数,如果我们自定义的是 Manager,那么 queryset_class 就是 QuerySet,如果是自定义的 QuerySet,最后调用了 as_manager 方法,这时,queryset_class 就是自定义的 QuerySet 。阅读循环体内部的代码,可以发现,如果 Manager已有额方法,跳过,如果方法的 queryset_only 为真,或者 queryset_only 为空的时候,方法名字以下划线开头的,跳过。这第二个跳过什么意思?queryset_only 意味着 该方法应该是 QuerySet 所独有,而下划线开头表示为私有方法。看看官方文档如何解释的
官方文档的描述就是我们分析的一个总结性东西。如果经常使用 Django ORM 就应该会发现 XXX.objects 确实没有 delete 方法。
接下来分析那个内部类,也是最绕的地方
def create_method(name, method):
def manager_method(self, *args, **kwargs):
return getattr(self.get_queryset(), name)(*args, **kwargs)
manager_method.__name__ = method.__name__
manager_method.__doc__ = method.__doc__
return manager_method
create_method 方法接受的是 方法名,和方法实体,正是 前面所讲的循环体内每次取出来的东西,需要注意的是,方法名和方法体是两码事,同一个方法名,可以指向别的方法体,这时调用的时候就变了。例如:
create_method 方法主要做了两件事,一是把 传递进来的方法体的名字和文档描述给了内部类 manager_method,另一个就是返回了 manager_method 这个方法本身。再看一下下面这段代码以及输出:
这说明方法名并不一定就是我们所见得那样,理解这一点也很重要。
前面的循环体代码最后是调用了 create_method 方法,也就是最后返回的是 manager_method 这个方法,如果我们不执行 manager_method 方法,那么 manager_method 内部的代码也不会执行。通过前面的分析,我们知道 _get_queryset_methods 方法是为了将 QuerySet 的方法拷贝给 新的动态生成的 Manager 类,因此我们要调用的 filter 方法,其实就是这里的 manager_method,一旦调用的时候,会发生什么呢?
先回顾一下下面的这段代码,接下来用得着。
@classmethod
def from_queryset(cls, queryset_class, class_name=None):
if class_name is None:
class_name = '%sFrom%s' % (cls.__name__, queryset_class.__name__)
return type(class_name, (cls,), {
'_queryset_class': queryset_class,
**cls._get_queryset_methods(queryset_class),
})
_get_queryset_methods 返回的是一个字典,在这里,一个新的字典变成了新类的类属性。于是调用了 filter 方法实际上就是 拿着 filter 这个名字取出对应的 方法体,也就是某一个 manager_method,filter() 表示调用了 filter,也就是调用了跟filter 对应的 manager_method,于是出发了 manager_method里面的代码,manager_method是一个内部函数,他所引用的 name 就是外部函数当时传递进来的name,在此处一定是 filter
, getattr(self.get_queryset(), name)(*args, **kwargs), getattr 是一个Python 方法,意为从 某个对象中取出某个值。self.get_queryset()一段代码就比较有意思了,self正是新生成的类构造的对象,这个新生成的类是 BaseManager 的间接子类,所以可以调用 get_queryset 方法,get_queryset 方法返回的又是一个类属性,这个类属性正是在构造新类的时候传递进来的,也就是我们自定义的这个QuerySet,于是 getattr(self.get_queryset(), name)(*args, **kwargs)
就变成了调用原来的 QuerySet 自己的 filter,这个构思不可谓不精妙