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,增强抽象级别。

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

感谢阅读。

建立一个更高级别的查询 API:正确使用Django ORM 的方式(转)的更多相关文章

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

    这个就比较深入啦... http://www.oschina.net/translate/higher-level-query-api-django-orm 结论: 在视图和其他高级应用中使用源生的O ...

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

    https://www.oschina.net/translate/higher-level-query-api-django-orm

  3. 从程序到系统:建立一个更智能的世界——记Joseph Sifakis“21世纪的计算”大会主题演讲

    Sifakis"21世纪的计算"大会主题演讲" title="从程序到系统:建立一个更智能的世界--记Joseph Sifakis"21世纪的计算&q ...

  4. python 之 Django框架(orm单表查询、orm多表查询、聚合查询、分组查询、F查询、 Q查询、事务、Django ORM执行原生SQL)

    12.329 orm单表查询 import os if __name__ == '__main__': # 指定当前py脚本需要加载的Django项目配置信息 os.environ.setdefaul ...

  5. 从零开始调用一个手机号归属地查询API

    自从过上程序员的生活,身边总是或多或少的提及一些API(应用程序接口),网上各种入门教程.实例大把大把,有的只是贴上部分代码,也不给注释, 写Demo的时候连编译都无法通过.下面我从小白开始来介绍下调 ...

  6. Django orm 常用查询筛选总结

    本文主要列举一下django orm中的常用查询的筛选方法: 大于.大于等于 小于.小于等于 in like is null / is not null 不等于/不包含于 其他模糊查询 model: ...

  7. 一个尖括号能干什么,画一个笑脸开始(为了支持交互,它又增添了JavaScript。HTML页面也越来越臃肿。于是CSS便诞生了。API和核心代码的出现使HTML能够访问更复杂的软件功能--支持更高级的交互和云服务集成。这就是今天的HTML5)

    一个尖括号 < 一个尖括号能干什么 < ? 你可以编出一顶帽子 <(:-p 或一张笑脸 :-> 再或者更直接一些 20世纪90年代初,html作为一种简单标记语言面世,用于在互 ...

  8. Qt 事件系统浅析 (用 Windows API 描述,分析了QCoreApplication::exec()和QEventLoop::exec的源码)(比起新号槽,事件机制是更高级的抽象,拥有更多特性,比如 accept/ignore,filter,还是实现状态机等高级 API 的基础)

    事件系统在 Qt 中扮演了十分重要的角色,不仅 GUI 的方方面面需要使用到事件系统,Signals/Slots 技术也离不开事件系统(多线程间).我们本文中暂且不描述 GUI 中的一些特殊情况,来说 ...

  9. MyBatis是一个支持普通SQL查询,存储过程和高级映射的优秀持久层框架

    MyBatis是一个支持普通SQL查询,存储过程和高级映射的优秀持久层框架.MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及对结果集的检索封装.MyBatis可以使用简单的XML或注解用 ...

随机推荐

  1. IIS中采用ISAPI-Rewrite防盗链

    本规则支持白名单排除式防盗链,搜索引擎友好(不屏蔽),被盗链后的错误提示转向,支持各种文件类型,经作者亲验真的能用,第一时间在itmop.com原创发表,请继续往下阅读. 近来小站遇到了盗链问题,至使 ...

  2. 系统日志:/var/log/messages

    /var/log/messages 存放的是系统的日志信息,它记录了各种事件,基本上什么应用都能往里写日志,在做故障诊断时可以首先查看该文件内容 [root@mirh5_center1_111.231 ...

  3. gcc的选项

    -g: 是一个编译选项,即在源代码编译的过程中起作用,让gcc把更多调试信息(也就包括符号信息)收集起来并将存放到最终的可执行文件内. -rdynamic:   却是一个 连接选项 ,它将指示连接器把 ...

  4. Swift - 类型转换(as as! as?)

    swift 类型转换 一,as 1,as使用场合 (1)从派生类转换为基类,向上转型(upcasts) class Animal {} class Cat: Animal {} let cat = C ...

  5. 从经典问题来看 Copy 方法

    经典面试题:为什么 NSString 类型成员变量的修饰属性用 copy 而不是 strong (或 retain ) ? 在初学 iOS 的时候,可能会被灌输这么一个常识,切记 NSString 的 ...

  6. 探求C#.Net中ArrayList与Array的区别

     ArrayList与Array的区别概述     ArrayList 是数组的复杂版本.ArrayList 类提供在大多数 Collections 类中提供但不在 Array 类中提供的一些功能.例 ...

  7. Matlab 二维绘图函数(plot类)

    plot 功能 绘制二维图形的最基本函数. 语法 //x为向量时,以x的元素值为纵坐标,x的序号为横坐标绘制曲线. //x为矩阵时,以其序号为横坐标,按列绘制每列元素值相对于其序号的曲线. polt( ...

  8. strcat的几种实现及性能比较

    一  原型说明 strcat()为C语言标准库函数,用于字符串拼接.函数原型声明在string.h头文件中: char *strcat(char *dest, const char *src); 该函 ...

  9. KMP算法的实现(Java语言描述)

    标签:it KMP算法是模式匹配专用算法. 它是在已知模式串的next或nextval数组的基础上执行的.如果不知道它们二者之一,就没法使用KMP算法,因此我们需要计算它们. KMP算法由两部分组成: ...

  10. H.264 White Paper学习笔记(一)总览

    H.264 White Paper对于264编码器的原理讲的比较透彻,在阅读学习的时候收获很大,这份文献网上有很多了,也有不少人翻译,不过想要理解更清楚我觉得还是得看英文原版的. 首先看一下白皮书里给 ...