利用Python的Django框架中的ORM建立查询API


Posted in Python onApril 20, 2015

 摘要

在这篇文章里,我将以反模式的角度来直接讨论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。在这个方法中,通过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...

更多代码,请参照Manager的资源文件。

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

你也可以在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()

这个方法有几个问题。

第一,这种实现方式比较??隆D阋??恳桓?uery自定义功能定义一个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()
>>> Todo.objects.all().high_priority()

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

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

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

方法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加进你的模型中,这能给你以下好处:

  •     ??麓?肷伲?⑶腋?∽场?/li>
  •     增加DRY,增强抽象级别。
  •    将所属的业务逻辑推送至对应的域模型层。

感谢阅读!

Python 相关文章推荐
python对json的相关操作实例详解
Jan 04 Python
详解 Python中LEGB和闭包及装饰器
Aug 03 Python
Python搭建FTP服务器的方法示例
Jan 19 Python
基于Python Numpy的数组array和矩阵matrix详解
Apr 04 Python
PYTHON绘制雷达图代码实例
Oct 15 Python
python每5分钟从kafka中提取数据的例子
Dec 23 Python
Windows下Pycharm远程连接虚拟机中Centos下的Python环境(图文教程详解)
Mar 19 Python
浅析Python面向对象编程
Jul 10 Python
python 字符串格式化的示例
Sep 21 Python
如何理解及使用Python闭包
Jun 01 Python
Pygame Time时间控制的具体使用详解
Nov 17 Python
使用scrapy实现增量式爬取方式
Jun 21 Python
对于Python的框架中一些会话程序的管理
Apr 20 #Python
介绍Python的Django框架中的QuerySets
Apr 20 #Python
使用Python的Django框架实现事务交易管理的教程
Apr 20 #Python
简化Python的Django框架代码的一些示例
Apr 20 #Python
在Python的Django框架上部署ORM库的教程
Apr 20 #Python
在Heroku云平台上部署Python的Django框架的教程
Apr 20 #Python
从Python程序中访问Java类的简单示例
Apr 20 #Python
You might like
ubuntu下配置nginx+php+mysql详解
2015/09/10 PHP
PHP操作MySQL的mysql_fetch_* 函数的常见用法教程
2015/12/25 PHP
php使用高斯算法实现图片的模糊处理功能示例
2016/11/11 PHP
PHP实现正则表达式分组捕获操作示例
2018/02/03 PHP
FireFox下XML对象转化成字符串的解决方法
2011/12/09 Javascript
JavaScript常用验证函数实例汇总
2014/11/25 Javascript
JavaScript中扩展Array contains方法实例
2020/08/23 Javascript
网站申请不到支付宝接口、微信接口,免接口收款实现方式几种解决办法
2016/12/14 Javascript
详解jQuery uploadify文件上传插件的使用方法
2016/12/16 Javascript
JS实现的简单拖拽功能示例
2017/03/13 Javascript
简单的vuex 的使用案例笔记
2018/04/13 Javascript
实例分析vue循环列表动态数据的处理方法
2018/09/28 Javascript
vue-video-player视频播放器使用配置详解
2020/10/23 Javascript
解决vue项目axios每次请求session不一致的问题
2020/10/24 Javascript
vuex中遇到的坑,vuex数据改变,组件中页面不渲染操作
2020/11/16 Javascript
python元组操作实例解析
2014/09/23 Python
Python Sql数据库增删改查操作简单封装
2016/04/18 Python
Zookeeper接口kazoo实例解析
2018/01/22 Python
python3.6.3+opencv3.3.0实现动态人脸捕获
2018/05/25 Python
Python3远程监控程序的实现方法
2019/07/15 Python
python实现批量修改服务器密码的方法
2019/08/13 Python
对Python中一维向量和一维向量转置相乘的方法详解
2019/08/26 Python
python3 assert 断言的使用详解 (区别于python2)
2019/11/27 Python
解决Keras中Embedding层masking与Concatenate层不可调和的问题
2020/06/18 Python
python获取命令行参数实例方法讲解
2020/11/02 Python
python中绕过反爬虫的方法总结
2020/11/25 Python
Python中读取文件名中的数字的实例详解
2020/12/25 Python
HTML5 LocalStorage 本地存储刷新值还在
2017/03/10 HTML / CSS
2015年简历自我评价范文
2015/03/11 职场文书
法律讲堂观后感
2015/06/11 职场文书
第一军规观后感
2015/06/12 职场文书
公司行政管理制度范本
2015/08/05 职场文书
《鸡兔同笼》教学反思
2016/02/19 职场文书
django如何自定义manage.py管理命令
2021/04/27 Python
Python装饰器的练习题
2021/11/23 Python
浅谈Node的内存泄露问题
2022/05/06 NodeJs