zoukankan      html  css  js  c++  java
  • 建立一个更高级别的查询 API:正确使用Django ORM 的方式(转)

    add by zhj: 本文作者是DabApps公司的技术主管,作者认为在view中直接使用Django提供的ORM查询方法是不好的,我对此并不赞同,可能作者

    写这篇文章是给Django的初学者看,所以在说明方法演进时有些罗嗦,至少方法1是没有必要说的。

    本文介绍了如何给QuerySet类增加方法属性。作者写本文时,Django1.7还在开发中,没有发布。在Django1.7版本中提供了这个功能,

    https://docs.djangoproject.com/en/dev/releases/1.7/#calling-custom-queryset-methods-from-the-manager。另:我对译文有修改。

    英文原文:http://www.dabapps.com/blog/higher-level-query-api-django-orm/

    译文原文:http://www.oschina.net/translate/higher-level-query-api-django-orm

    译者:Naixjs, fkkeee, 晴风晓月

    摘要

    在这篇文章里,我认为在view中直接使用Django的低级的ORM查询方法(如filter, order_by等)通常是反模式的。作为一种替代方式,我们需要在模型层建立

    查询API,这在Django中做起来不是非常容易,但通过深入地了解ORM的内部,我将告诉你一些简捷的方式来达到这个目的。

    概览

    当编写Django应用程序时,我们已经习惯通过添加method到model中以此达到封装业务逻辑并隐藏实现细节。这种方法看起来是非常的自然,而且实际上它也用在

    Django的内建应用中。

    from django.contrib.auth.models import User
    user = User.objects.get(pk=5)
    user.set_password('super-sekrit')
    user.save()

    这里的set_password就是一个定义在django.contrib.auth.models.User这个model中的方法,它隐藏了对密码进行哈希操作的具体实现。相应的代码看起来应该

    是这样:

    from django.contrib.auth.hashers import make_password
    
    class User(models.Model):
    
        # fields go here..
    
        def set_password(self, raw_password):
            self.password = make_password(raw_password)

    这样做的好处是使代码更具可读性、重用性和健壮性。

    我们已经在单独的例子中这样做了,下面将会把它用在获取数据库信息的例子中。

    为了描述这个方法,我们使用了一个简单的app(todo list)来说明。注意:这是一个例子,因为很难用少量的代码展示一个真实的例子。

    下面就是models.py文件:

    from django.db import models
    
    PRIORITY_CHOICES = [(1, 'High'), (2, 'Low')]
    
    class Todo(models.Model):
        content = models.CharField(max_length=100)
        is_done = models.BooleanField(default=False)
        owner = models.ForeignKey('auth.User')
        priority = models.IntegerField(choices=PRIORITY_CHOICES, default=1

    想像一下,我们需要查询当前用户所有不完整的,高优先级的 Todos。这里是代码:

    def dashboard(request):
    
        todos = Todo.objects.filter(
            owner=request.user
        ).filter(
            is_done=False
        ).filter(
            priority=1
        )
    
        return render(request, 'todos/list.html', {
            'todos': todos,
        })

    注意:这里可以写成request.user.todo_set.filter(is_done=False, priority=1)。但是记住这里只是一个实验。

    为什么这样写不好呢?

    首先,代码冗长。七行代码才能完成,正式的项目中,将会更加复杂。

    其次,泄露实现细节。比如我们需要知道model中有一个名为is_done的布尔型字段,如果你将字段类型修改为有多个允许值的field,那这个代码就不能用了。

    然后就是,意图不清晰,很难理解。

    最后,使用中会有重复。例:你需要写一个management command,每周给每个用户发送他自己的todo list,这时候你就需要复制-粘贴着七行代码。这不符合

    DRY(do not repeat yourself)

    让我们总结一下:直接使用低等级的ORM代码是反模式的。

    如何改进呢?

    使用 Managers 和 QuerySets

    首先,让我们先了解一下概念。

    Django 有两个关系密切的与表级别操作相关的结构:managers 和 querysets

    manager(django.db.models.manager.Manager的一个实例)被描述成 “为model提供查询数据库操作的接口”。Manager是通往表级功能的大门。每一个model

    都有一个默认的manager,叫做objects。

    Quesyset (django.db.models.query.QuerySet) 是“数据库中objects的集合”。本质上是一个lazy SELECT查询,也可以使用过滤,排序等(filtered,ordered),

    来限制或者修改查询 到的数据。用它来创建或操纵 django.db.models.sql.query.Query实例,然后在数据库后台转换成SQL查询。

    啊?你还不明白?

    随着你慢慢深入的了解ORM,你就会明白Manager和QuerySet之间的区别了。

    人们会被所熟知的Manager接口搞糊涂,因为他并不是看上去那样。

    Manager接口就是个谎言。

    QuerySet方法是可链式调用的。每一次调用QuerySet的方法(如:filter)都会返回一个复制的queryset等待下一次的调用。这也是Django ORM 流畅之美的一部分。

    QuerySet 的所有方法需要在Manager中要重新实现,从而通过model.objects也可以实现链式调用。这些方法在Manager中只是QuerySet中对应方法的代理,通过

    self.get_query_set(),如下

    class Manager(object):
    
        # SNIP some housekeeping stuff..
    
        def get_query_set(self):
            return QuerySet(self.model, using=self._db)
    
        def all(self):
            return self.get_query_set()
    
        def count(self):
            return self.get_query_set().count()
    
        def filter(self, *args, **kwargs):
            return self.get_query_set().filter(*args, **kwargs)
    
        # and so on for 100+ lines...

    让我们立刻回到todo list ,解决query接口的问题。Django推荐的方法是自定义Manager子类,并加在models中。

    class IncompleteTodoManager(models.Manager):
        def get_query_set(self):
            return super(TodoManager, self).get_query_set().filter(is_done=False)
    
    class HighPriorityTodoManager(models.Manager):
        def get_query_set(self):
            return super(TodoManager, self).get_query_set().filter(priority=1)
    
    class Todo(models.Model):
        content = models.CharField(max_length=100)
        # other fields go here..
    
        objects = models.Manager() # the default manager
    
        # attach our custom managers:
        incomplete = models.IncompleteTodoManager()
        high_priority = models.HighPriorityTodoManager()

    你也可以在model中增加多个managers,或者重新定义objects,也可以维持单个的manager,增加自定义方法。

    下面让我们实验一下这几种方法:

    方法1:多managers

    class IncompleteTodoManager(models.Manager):
        def get_query_set(self):
            return super(TodoManager, self).get_query_set().filter(is_done=False)
    
    class HighPriorityTodoManager(models.Manager):
        def get_query_set(self):
            return super(TodoManager, self).get_query_set().filter(priority=1)
    
    class Todo(models.Model):
        content = models.CharField(max_length=100)
        # other fields go here..
    
        objects = models.Manager() # the default manager
    
        # attach our custom managers:
        incomplete = models.IncompleteTodoManager()
        high_priority = models.HighPriorityTodoManager()

    我们的API看起来是这样:

    >>> Todo.incomplete.all()
    >>> Todo.high_priority.all()

    这个方法有几个问题。

    第一,这种实现方式比较啰嗦。你要为每一个自定义查询定义一个manager。

    第二,这将会弄乱你的命名空间。因为Django开发者习惯把Model.objects看做表的入口,而这种方法会破坏这个规则。

    第 三,不可链式调用。还是要用低等级的ORM代码实现:Todo.incomplete.filter(priority=1) 或Todo.high_priority.filter(is_done=False)

    综上,使用多managers的方法,不是最优选择。

    方法2: Manager 方法

    现在,我们试下其他Django允许的方法:在单个自定义Manager中的多个方法

    class TodoManager(models.Manager):
        def incomplete(self):
            return self.filter(is_done=False)
    
        def high_priority(self):
            return self.filter(priority=1)
    
    class Todo(models.Model):
        content = models.CharField(max_length=100)
        # other fields go here..
    
        objects = TodoManager()

    我们的API 现在看起来是这样:

    >>> Todo.objects.incomplete()
    >>> Todo.objects.high_priority()

    这个方法显然更好。它没有太多累赘(只有一个Manager类)并且可以很方便地添加更多的方法。

    不过仍然不能链式调用自定义方法,因为Todo.objects.incomplete() 和Todo.objects.high_priority()返回的都是Django QuerySet类的实例,所以我们

    无法使用Todo.objects.incomplete().high_priority() 。

    方法3:自定义QuerySet

    现在我们已进入Django尚未开发的领域,Django文档中找不到这些内容。

    class TodoQuerySet(models.query.QuerySet):
        def incomplete(self):
            return self.filter(is_done=False)
    
        def high_priority(self):
            return self.filter(priority=1)
    
    class TodoManager(models.Manager):
        def get_query_set(self):
            return TodoQuerySet(self.model, using=self._db)
    
    class Todo(models.Model):
        content = models.CharField(max_length=100)
        # other fields go here..
    
        objects = TodoManager()

    我们从以下调用的视图代码中可以看出端倪:

    >>> Todo.objects.get_query_set().incomplete()
    >>> Todo.objects.get_query_set().high_priority()
    >>> # (or)
    >>> Todo.objects.all().incomplete()
    >>> Todo.objects.all().high_priority()

    差不多完成了!这并有比第2个方法多多少累赘,得到方法2同样的好处,和额外的效果(来点鼓声吧...),它终于可链式查询了!

    >>> Todo.objects.all().incomplete().high_priority()

    然而它还不够完美。这个自定义的Manager仅仅是一个样板而已,而且 all() 还有瑕疵,在使用时不好把握,而更重要的是不兼容,它让我们的代码看起来有点怪异。

    方法3a:复制Django,代理做所有事

    我们简单地在Manager中重新定义所有QuerySet方法

    QuerySet:

    class TodoQuerySet(models.query.QuerySet):
        def incomplete(self):
            return self.filter(is_done=False)
    
        def high_priority(self):
            return self.filter(priority=1)
    
    class TodoManager(models.Manager):
        def get_query_set(self):
            return TodoQuerySet(self.model, using=self._db)
    
        def incomplete(self):
            return self.get_query_set().incomplete()
    
        def high_priority(self):
            return self.get_query_set().high_priority()

    这个能更好地提供我们想要的API:

    >>> Todo.objects.incomplete().high_priority() # yay!

    但代码冗余、且不符合DRY,每次你新增一个文件到QuerySet,或是更改现有的方法标记,你必须记住在你的Manager中做相同的更改,否则它可能不会正常工作。

    方法3b: django-model-utils

    Python 是一种动态语言,我们可以做到DRY吗?答案是肯定的,要通过一个名叫Django-model-utils的第三方应用帮忙。运行 pip install django-model-utils ,

    然后……

    from model_utils.managers import PassThroughManager
    
    class TodoQuerySet(models.query.QuerySet):
        def incomplete(self):
            return self.filter(is_done=False)
    
        def high_priority(self):
            return self.filter(priority=1)
    
    class Todo(models.Model):
        content = models.CharField(max_length=100)
        # other fields go here..
    
        objects = PassThroughManager.for_queryset_class(TodoQuerySet)()

    这要好多了。我们只是定义QuerySet子类,然后通过django-model-utils提供的PassThroughManager类附加这个自定义QuerySet子类到我们的model中。

    PassThroughManager 是由__getattr__ 实现的,当调用objects不存在的方法时,它会自动代理它们到QuerySet。这里需要小心一点,检查确认我们没有在一

    些特性中没有无限递归(这是我为什么推荐使用django-model-utils,而不是自己手工写)。

    做这些有什么帮助?

    记得之前定义的view吗?

    def dashboard(request):
    
        todos = Todo.objects.filter(
            owner=request.user
        ).filter(
            is_done=False
        ).filter(
            priority=1
        )
    
        return render(request, 'todos/list.html', {
            'todos': todos,
        })

    加点小改动,它看起来是这样:

    def dashboard(request):
    
        todos = Todo.objects.for_user(
            request.user
        ).incomplete().high_priority()
    
        return render(request, 'todos/list.html', {
            'todos': todos,
        })

    希望你也能同意第二个版本比第一个更简便,清晰并且更有可读性。

    Django能帮忙么?

    让这整个事情更容易的方法,已经在django开发邮件列表中讨论过,下面是Zachary Voase的建议:

    class TodoManager(models.Manager):
    
        @models.querymethod
        def incomplete(query):
            return query.filter(is_done=False)

    通过这个简单的装饰方法的定义,让Manager和QuerySet都能使不可用的方法神奇地变为可用。

    我个人并不完全赞同使用装饰器的方法。它略过了详细的信息,感觉有点“嘻哈”。我感觉好的方法是增加一个QuerSet子类(而不是Manager子类)。

    或者我们更进一步思考。退回到在争议中重新审视Django的API设计决定时,也许我们能得到真实更深的改进。可以消除Managers和QuerySet的区别吗

    (或者至少使这个区别更明显)?

    我很确信,不管以前是否曾经有过这么大的重构工作,这个功能必然要在Django 2.0 甚至更后的版本中。

    因此,简单概括一下:

    在视图和其他高级应用中使用源生的ORM查询代码不是很好的主意。而是用django-model-utils中的PassThroughManager将我们新加的自定义QuerySet API

    加进你的模型中,这能给你以下好处:

       啰嗦代码少,并且更健壮。

       增加DRY,增强抽象级别。

      将所属的业务逻辑推送至模型层实现。

    感谢阅读。

  • 相关阅读:
    HDOJ-3635-Dragon Balls 解题报告
    HDOJ-1217-Arbitrage 解题报告
    C语言控制台窗口图形界面编程(总结)
    PC(win10)上搭建 kubernetes + docker 集群环境
    Hessian 2.0 序列化协议
    对响应式布局的理解
    常用链接集合
    dubbox升级dubbo的过渡方案:通过扩展dubbo的Protocol实现dubbo与dubbox的相互调用
    Wordpress部署 —— 基于Ubuntu、Mysql和Nginx
    关于27种常见设计模式的总结
  • 原文地址:https://www.cnblogs.com/ajianbeyourself/p/3840681.html
Copyright © 2011-2022 走看看