首頁 後端開發 Python教學 利用Python的Django框架中的ORM建立查询API

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

Jun 06, 2016 am 11:25 AM
python

 摘要

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

概览

当编写Django应用程序时,我们已经习惯通过添加方法到模型里以此达到封装业务逻辑并隐藏实现细节。这种方法看起来是非常的自然,而且实际上它也用在Django的内建应用中。
 

1

2

3

4

>>> 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模型中的方法,它隐藏了对密码进行哈希操作的具体实现。相应的代码看起来应该是这样:

1

2

3

4

5

6

7

8

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文件:

1

2

3

4

5

6

7

8

9

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。这里是代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

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()的代理,重新创建一个

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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()

登入後複製

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

1

2

>>> 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中的多个方法

1

2

3

4

5

6

7

8

9

10

11

12

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 现在看起来是这样:

1

2

>>> 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文档中找不到这些内容。。。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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()

登入後複製

我们从以下调用的视图代码中可以看出端倪:


1

2

3

4

5

>>> 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同样的好处,和额外的效果(来点鼓声吧...),它终于可链式查询了!

1

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

登入後複製

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


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

现在我们让以上”假冒Manager API“讨论变得有用:我们知道如何解决这个问题。我们简单地在Manager中重新定义所有QuerySet方法,然后代理它们返回我们自定义QuerySet:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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:

1

>>> Todo.objects.incomplete().high_priority() # yay!

登入後複製

除上面那些输入部分、且非常不DRY,每次你新增一个文件到QuerySet,或是更改现有的方法标记,你必须记住在你的Manager中做相同的更改,否则它可能不会正常工作。这是配置的问题
方法3b: django-model-utils

Python 是一种动态语言。 我们就一定能避免所有模块?一个名叫Django-model-utils的第三方应用带来的一点小忙,就会有点不受控制了。先运行 pip install django-model-utils ,然后……

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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所提供的用不断尝试测试的方法,而不是自己手工重复写)。

做这些有什么帮助?

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

1

2

3

4

5

6

7

8

9

10

11

12

13

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,

  })

登入後複製
登入後複製

加点小改动,我们让它看起来象这样:

1

2

3

4

5

6

7

8

9

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则建议如下:

1

2

3

4

5

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

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
4 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
4 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1673
14
CakePHP 教程
1429
52
Laravel 教程
1333
25
PHP教程
1278
29
C# 教程
1257
24
PHP和Python:解釋了不同的範例 PHP和Python:解釋了不同的範例 Apr 18, 2025 am 12:26 AM

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

在PHP和Python之間進行選擇:指南 在PHP和Python之間進行選擇:指南 Apr 18, 2025 am 12:24 AM

PHP適合網頁開發和快速原型開發,Python適用於數據科學和機器學習。 1.PHP用於動態網頁開發,語法簡單,適合快速開發。 2.Python語法簡潔,適用於多領域,庫生態系統強大。

sublime怎麼運行代碼python sublime怎麼運行代碼python Apr 16, 2025 am 08:48 AM

在 Sublime Text 中運行 Python 代碼,需先安裝 Python 插件,再創建 .py 文件並編寫代碼,最後按 Ctrl B 運行代碼,輸出會在控制台中顯示。

PHP和Python:深入了解他們的歷史 PHP和Python:深入了解他們的歷史 Apr 18, 2025 am 12:25 AM

PHP起源於1994年,由RasmusLerdorf開發,最初用於跟踪網站訪問者,逐漸演變為服務器端腳本語言,廣泛應用於網頁開發。 Python由GuidovanRossum於1980年代末開發,1991年首次發布,強調代碼可讀性和簡潔性,適用於科學計算、數據分析等領域。

Python vs. JavaScript:學習曲線和易用性 Python vs. JavaScript:學習曲線和易用性 Apr 16, 2025 am 12:12 AM

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

Golang vs. Python:性能和可伸縮性 Golang vs. Python:性能和可伸縮性 Apr 19, 2025 am 12:18 AM

Golang在性能和可擴展性方面優於Python。 1)Golang的編譯型特性和高效並發模型使其在高並發場景下表現出色。 2)Python作為解釋型語言,執行速度較慢,但通過工具如Cython可優化性能。

vscode在哪寫代碼 vscode在哪寫代碼 Apr 15, 2025 pm 09:54 PM

在 Visual Studio Code(VSCode)中編寫代碼簡單易行,只需安裝 VSCode、創建項目、選擇語言、創建文件、編寫代碼、保存並運行即可。 VSCode 的優點包括跨平台、免費開源、強大功能、擴展豐富,以及輕量快速。

notepad 怎麼運行python notepad 怎麼運行python Apr 16, 2025 pm 07:33 PM

在 Notepad 中運行 Python 代碼需要安裝 Python 可執行文件和 NppExec 插件。安裝 Python 並為其添加 PATH 後,在 NppExec 插件中配置命令為“python”、參數為“{CURRENT_DIRECTORY}{FILE_NAME}”,即可在 Notepad 中通過快捷鍵“F6”運行 Python 代碼。

See all articles