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


摘要

在这篇文章里,我将以反模式的角度来直接讨论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
如何解决Linux终端中查看Python版本时遇到的权限问题?如何解决Linux终端中查看Python版本时遇到的权限问题?Apr 01, 2025 pm 05:09 PM

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

我如何使用美丽的汤来解析HTML?我如何使用美丽的汤来解析HTML?Mar 10, 2025 pm 06:54 PM

本文解释了如何使用美丽的汤库来解析html。 它详细介绍了常见方法,例如find(),find_all(),select()和get_text(),以用于数据提取,处理不同的HTML结构和错误以及替代方案(SEL)

Python中的数学模块:统计Python中的数学模块:统计Mar 09, 2025 am 11:40 AM

Python的statistics模块提供强大的数据统计分析功能,帮助我们快速理解数据整体特征,例如生物统计学和商业分析等领域。无需逐个查看数据点,只需查看均值或方差等统计量,即可发现原始数据中可能被忽略的趋势和特征,并更轻松、有效地比较大型数据集。 本教程将介绍如何计算平均值和衡量数据集的离散程度。除非另有说明,本模块中的所有函数都支持使用mean()函数计算平均值,而非简单的求和平均。 也可使用浮点数。 import random import statistics from fracti

如何使用TensorFlow或Pytorch进行深度学习?如何使用TensorFlow或Pytorch进行深度学习?Mar 10, 2025 pm 06:52 PM

本文比较了Tensorflow和Pytorch的深度学习。 它详细介绍了所涉及的步骤:数据准备,模型构建,培训,评估和部署。 框架之间的关键差异,特别是关于计算刻度的

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

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

如何使用Python创建命令行接口(CLI)?如何使用Python创建命令行接口(CLI)?Mar 10, 2025 pm 06:48 PM

本文指导Python开发人员构建命令行界面(CLIS)。 它使用Typer,Click和ArgParse等库详细介绍,强调输入/输出处理,并促进用户友好的设计模式,以提高CLI可用性。

哪些流行的Python库及其用途?哪些流行的Python库及其用途?Mar 21, 2025 pm 06:46 PM

本文讨论了诸如Numpy,Pandas,Matplotlib,Scikit-Learn,Tensorflow,Tensorflow,Django,Blask和请求等流行的Python库,并详细介绍了它们在科学计算,数据分析,可视化,机器学习,网络开发和H中的用途

解释Python中虚拟环境的目的。解释Python中虚拟环境的目的。Mar 19, 2025 pm 02:27 PM

文章讨论了虚拟环境在Python中的作用,重点是管理项目依赖性并避免冲突。它详细介绍了他们在改善项目管理和减少依赖问题方面的创建,激活和利益。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

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

AI Clothes Remover

AI Clothes Remover

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

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前By尊渡假赌尊渡假赌尊渡假赌

热工具

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

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

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

螳螂BT

螳螂BT

Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

mPDF

mPDF

mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),