標籤:style blog http color 使用 strong
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 Useruser = 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_passwordclass 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 modelsPRIORITY_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 PassThroughManagerclass 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,增強抽象層級。
將所屬的商務邏輯推送至模型層實現。
感謝閱讀。