首页 后端开发 Python教程 建立一个更高级别的查询 API:正确使用Django ORM 的方式

建立一个更高级别的查询 API:正确使用Django ORM 的方式

Oct 17, 2016 pm 02:29 PM


摘要

在这篇文章里,我将以反模式的角度来直接讨论Django的低级ORM查询方法的使用。作为一种替代方式,我们需要在包含业务逻辑的模型层建立与特定领域相关的查询API,这些在Django中做起来不是非常容易,但通过深入地了解ORM的内容原理,我将告诉你一些简捷的方式来达到这个目的。

概览

当编写Django应用程序时,我们已经习惯通过添加方法到模型里以此达到封装业务逻辑并隐藏实现细节。这种方法看起来是非常的自然,而且实际上它也用在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模型中的方法,它隐藏了对密码进行哈希操作的具体实现。相应的代码看起来应该是这样:

   
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)
登录后复制

我们正在使用Django,建立一个特定领域的顶部通用接口,低等级的ORM工具。在此基础上,增加抽象等级,减少交互代码。这样做的好处是使代码更具可读性、重用性和健壮性。

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

为了描述这个方法,我们使用了一个简单的app(todo list)来说明。

注意:这是一个例子。因为很难用少量的代码展示一个真实的例子。不要过多的关心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
登录后复制

想像一下,我们将要传递这些数据,建立一个view,来为当前用户展示不完整的,高优先级的 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)。但是这里只是一个实验。

为什么这样写不好呢?

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

其次,泄露实现细节。比如代码中的is_done是BooleanField,如果改变了他的类型,代码就不能用了。

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

最后,使用中会有重复。例:你需要写一行命令,通过cron,每周发送给所有用户一个todo list,这时候你就需要复制-粘贴着七行代码。这不符合DRY(do not repeat yourself)

让我们大胆的猜测一下:直接使用低等级的ORM代码是反模式的。

如何改进呢?

使用 Managers 和 QuerySets

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

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

manager(django.db.models.manager.Manager的一个实例)被描述成 “通过查询数据库提供给Django的插件”。Manager是表级别功能的通往ORM大门。每一个model都有一个默认的manager,叫做objects。

Quesyset (django.db.models.query.QuerySet) 是“数据库中objects的集合”。本质上是一个SELECT查询,也可以使用过滤,排序等(filtered,ordered),来限制或者修改查询到的数据。用来 创建或操纵 django.db.models.sql.query.Query实例,然后通过数据库后端在真正的SQL中查询。


啊?你还不明白?

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

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

Manager接口就是个谎言。

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

但是当Model.objects 是一个 Manager时,就出现问题了。我们需要调用objects作为开始,然后链接到结果的QuerySet上去。

那么Django又是如何解决呢?

接口的谎言由此暴露,所有的QuerySet 方法基于Manager。在这个方法中,通过

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

self.get_query_set()的代理,重新创建一个QuerySet。
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...
登录后复制


你也可以在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()
登录后复制

这个接口将以这样的方式展现:

>>> Todo.incomplete.all()
>>> Todo.high_priority.all()
登录后复制

这个方法有几个问题。

第一,这种实现方式比较啰嗦。你要为每一个query自定义功能定义一个class。

第二,这将会弄乱你的命名空间。Django开发者吧Model.objects看做表的入口。这样做会破坏命名规则。

第三,不可链接的。这样做不能将managers组合在一起,获得不完整,高优先级的todos,还是回到低等级的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.incomplete().high_priority() 。我们卡在 Todo.objects.incomplete().filter(is_done=False),没有使用。

方法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()
登录后复制

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

>>> Todo.objects.all().incomplete().high_priority()
登录后复制

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

>>> Todo.objects.all().high_priority()
登录后复制

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

现在我们让以上”假冒Manager API“讨论变得有用:我们知道如何解决这个问题。我们简单地在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 是一种动态语言。 我们就一定能避免所有模块?一个名叫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__ 实现的,它能阻止访问到django定义的“不存在的方法”,并且自动代理它们到QuerySet。这里需要小心一点,检查确认我们没有在一些特性中没有无限递归(这是我为什么推荐使用django-model-utils所提供的用不断尝试测试的方法,而不是自己手工重复写)。

做这些有什么帮助?

记得上面早些定义的视图代码么?

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开发邮件列表中讨论过,并且得到一个相关票据(译注:associated ticket叫啥名更好?)。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,增强抽象级别。

  将所属的业务逻辑推送至对应的域模型层。


本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

如何解决Linux终端中查看Python版本时遇到的权限问题? 如何解决Linux终端中查看Python版本时遇到的权限问题? Apr 01, 2025 pm 05:09 PM

Linux终端中查看Python版本时遇到权限问题的解决方法当你在Linux终端中尝试查看Python的版本时,输入python...

如何在使用 Fiddler Everywhere 进行中间人读取时避免被浏览器检测到? 如何在使用 Fiddler Everywhere 进行中间人读取时避免被浏览器检测到? Apr 02, 2025 am 07:15 AM

使用FiddlerEverywhere进行中间人读取时如何避免被检测到当你使用FiddlerEverywhere...

在Python中如何高效地将一个DataFrame的整列复制到另一个结构不同的DataFrame中? 在Python中如何高效地将一个DataFrame的整列复制到另一个结构不同的DataFrame中? Apr 01, 2025 pm 11:15 PM

在使用Python的pandas库时,如何在两个结构不同的DataFrame之间进行整列复制是一个常见的问题。假设我们有两个Dat...

如何在10小时内通过项目和问题驱动的方式教计算机小白编程基础? 如何在10小时内通过项目和问题驱动的方式教计算机小白编程基础? Apr 02, 2025 am 07:18 AM

如何在10小时内教计算机小白编程基础?如果你只有10个小时来教计算机小白一些编程知识,你会选择教些什么�...

Uvicorn是如何在没有serve_forever()的情况下持续监听HTTP请求的? Uvicorn是如何在没有serve_forever()的情况下持续监听HTTP请求的? Apr 01, 2025 pm 10:51 PM

Uvicorn是如何持续监听HTTP请求的?Uvicorn是一个基于ASGI的轻量级Web服务器,其核心功能之一便是监听HTTP请求并进�...

在Linux终端中使用python --version命令时如何解决权限问题? 在Linux终端中使用python --version命令时如何解决权限问题? Apr 02, 2025 am 06:36 AM

Linux终端中使用python...

如何绕过Investing.com的反爬虫机制获取新闻数据? 如何绕过Investing.com的反爬虫机制获取新闻数据? Apr 02, 2025 am 07:03 AM

攻克Investing.com的反爬虫策略许多人尝试爬取Investing.com(https://cn.investing.com/news/latest-news)的新闻数据时,常常�...

See all articles