建立一個更進階別的查詢 API:正確使用Django ORM 的方式

來源:互聯網
上載者:User

標籤: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,增強抽象層級。

  將所屬的商務邏輯推送至模型層實現。

感謝閱讀。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.