利用Python的Django框架中的ORM建立查询API
摘要 在这篇文章里,我将以反模式的角度来直接讨论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)
我们已经在单独的例子中这样做了,下面将会把它用在获取数据库信息的例子中。 为了描述这个方法,我们使用了一个简单的app(todo list)来说明。 注意:这是一个例子。因为很难用少量的代码展示一个真实的例子。不要过多的关心todo list继承他自己,而要把重点放在如何让这个方法运行。 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
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)
使用 Managers 和 QuerySets Django 有两个关系密切的与表级别操作相关的构图:managers 和 querysets manager(django.db.models.manager.Manager的一个实例)被描述成 “通过查询数据库提供给Django的插件”。Manager是表级别功能的通往ORM大门。每一个model都有一个默认的manager,叫做objects。 啊?你还不明白? 随着你慢慢深入的了解ORM,你就会明白Manager和QuerySet之间的区别了。
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() 这个方法有几个问题。
第二,这将会弄乱你的命名空间。Django开发者吧Model.objects看做表的入口。这样做会破坏命名规则。 第三,不可链接的。这样做不能将managers组合在一起,获得不完整,高优先级的todos,还是回到低等级的ORM代码:Todo.incomplete.filter(priority=1) 或Todo.high_priority.filter(is_done=False)
现在,我们试下其他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类)并且这种查询方法很好地在对象后预留命名空间。(译注:可以很形象、方便地添加更多的方法)
现在我们已进入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() 还有瑕疵,在使用时不好把握,而更重要的是不兼容,它让我们的代码看起来有点怪异。
现在我们让以上”假冒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中做相同的更改,否则它可能不会正常工作。这是配置的问题 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,}) 加点小改动,我们让它看起来象这样: def dashboard(request): todos = Todo.objects.for_user( request.user ).incomplete().high_priority() return render(request,}) 希望你也能同意第二个版本比第一个更简便,清晰并且更有可读性。
class TodoManager(models.Manager): @models.querymethod def incomplete(query): return query.filter(is_done=False) 通过这个简单的装饰方法的定义,让Manager和QuerySet都能使不可用的方法神奇地变为可用。 我个人并不完全赞同使用基于装饰方法。它略过了详细的信息,感觉有点“嘻哈”。我感觉好的方法,增加一个QuerSet子类(而不是Manager子类)是更好,更简单的途径。
因此,简单概括一下: 在视图和其他高级应用中使用源生的ORM查询代码不是很好的主意。而是用django-model-utils中的PassThroughManager将我们新加的自定义QuerySet API加进你的模型中,这能给你以下好处:
感谢阅读! 您可能感兴趣的文章:
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |