首页 > 其他分享 >001 自定义QuerySet

001 自定义QuerySet

时间:2022-12-25 22:56:13浏览次数:56  
标签:name 自定义 QuerySet queryset method 001 utm class

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,这个构思不可谓不精妙

标签:name,自定义,QuerySet,queryset,method,001,utm,class
From: https://www.cnblogs.com/yaowy001/p/16999805.html

相关文章