iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 17
0
Modern Web

加速你的 Django 網站開發 - Django 的好用套件系列 第 17

17. django-model-utils

在設計 Model 時,其實有蠻多情況都是重複的,例如 Timestamp、軟刪除、時序資料等等,每次都要重新寫一次,這就違反了 DRY 原則。這時候,你可以使用 django-model-utils 這個套件,這個套件就把剛剛我們提到的情況提取出來成為類別,我們只要簡單的繼承,就可以讓 model 可以處理這些情況。

文件網址:https://django-model-utils.readthedocs.io/en/latest/

安裝

poetry add django-model-utils

安裝好就可以使用了,不需要設定。

使用

django-model-utils 主要提供這三種類別:

  • Field
  • Model
  • Model Manager

欄位 (Field)

主要有提供這三種 Field 可以使用:

  1. StatusField
  2. MonitorField
  3. SplitField

StatusField 適合用在狀態或是有固定選項的欄位,例如:草稿/已發佈。

from model_utils.fields import StatusField
from model_utils import Choices

class Article(models.Model):
    ANOTHER_CHOICES = Choices('draft', 'published')
    # ...
    another_field = StatusField(choices_name='ANOTHER_CHOICES')

MonitorField 可以用來監控其他 Field 的更動,並紀錄更動時間。實際上是繼承 DateTimeField,所以可以紀錄時間。

from model_utils.fields import MonitorField, StatusField
from model_utils import Choices

class Article(models.Model):
    STATUS = Choices('draft', 'published')

    status = StatusField(choices_name='STATUS')
    status_changed = MonitorField(monitor='status')

SplitField 基本上就 TextField,但是它額外提供了 excerpt, content, has_more 這3個屬性,簡單的說,它可以用來儲存文章內容與摘錄。

from django.db import models
from model_utils.fields import SplitField

class Article(models.Model):
    title = models.CharField(max_length=100)
    body = SplitField()

在這樣宣告 model 以後,我們可以這樣使用:

>>> a = Article.objects.all()[0]
>>> a.body.content
u'some text\n\n<!-- split -->\n\nmore text'
>>> a.body.excerpt
u'some text\n'
>>> unicode(a.body)
u'some text\n\n<!-- split -->\n\nmore text'

使用 a.body.content / a.body 就可以取得 body 完整內容,使用 a.body.excerpt 則會取得摘錄。

那你會看到 a.body.content 裡有 <!-- split --> 的字串,這個是 SplitField 裡用來取得摘要的分隔字串,我們可以在 settings 裡指定 SPLIT_MARKER 變數的內容來作為分隔字串。

Model

django-model-utils 提供了幾個現成的 abstract model ,在繼承以後,會擁有一定的特殊能力。

主要有這幾個:

  • TimeFramedModel
  • TimeStampedModel
  • StatusModel
  • SoftDeletableModel

TimeFramedModel

這個 model 主要是處理時間序,繼承以後,model 裡面有 start / end 兩個 DateTimeField 以及 timeframed manager 來方便你做時間序的操作。

TimeStampedModel

這個 model 主要是處理時間戳記,繼承以後,model 裡面有 created / modified 兩個 DateTimeField。

StatusModel

這個 model 主要是處理狀態,繼承以後,model 裡面有 status 與 status_changed 兩個欄位,這兩個欄位分別是 StatusField 與 MonitorField 所宣告的。所以可以記錄狀態,也可以記錄狀態變更的時間。在使用的時候,需要宣告有什麼狀態。

# models.py
from model_utils.models import StatusModel
from model_utils import Choices

class Article(StatusModel):
    STATUS = Choices('draft', 'published')

在使用的時候,就可以這樣使用

from .models import Article

a = Article()
a.status = Article.STATUS.published

# 儲存時,會更新 status_changed 這個欄位。
a.save()

# 只查詢已經發佈 (published) 的文章。
Article.published.all()

SoftDeletableModel

在很多時候,資料是不真正刪除的,而是只需要標記為刪除,這時候 SoftDeletableModel 就可以派上用場,繼承 SopftDeletableModel 以後,裡面會提供一個 is_removed 欄位,將 is_removed 設為真,就當做是真的刪除了。在查詢的時候,會自動加上 is_removed=False 來篩選,而不需要另外自己處理。

Model Managers

Manager 是 Django 裡的一個用詞,主要用途是提供一組統一的對資料庫的操作介面,django-model-utils 提供了一些特別的 manager:

  • InheritanceManager
  • JoinManager
  • QueryManager
  • SoftDeletableManager

InheritanceManager

這個 manager 適用在有繼承關係的 model ,我們直接看例子比較快

from django.db import models
from model_utils.managers import InheritanceManager

class Place(models.Model):
    objects = InheritanceManager()
    name = models.CharField(max_length=256)

class Restaurant(Place):
    chef = models.CharField(max_length=32)

class Bar(Place):
    bartender = models.CharField(max_length=32)

在上面這段程式裡, Restaurant 跟 Bar 都繼承了 Place。那麼,如果我想使用 Place 查詢到 Restaurant 跟 Bar 的話,可以這樣查詢

places = Place.objects.select_subclasses(Restaurant, Bar)
places = Place.objects.select_subclasses()
places = Place.objects.select_subclasses(Restaurant)

你可能會問,這跟 Place.objects.all() 有什麼不一樣?

不一樣在於,使用 Place.objects.select_subclasses() 所得到的結果,裡面物件的型態會是 Restaurant 或 Bar,不會是 Place。

這邊要注意的一個地方是使用 Place.objects.select_subclasses(Restaurant) 並不是只拿到Restaurant 型態的物件,而是除 Restaurant 型態的物件之外,其他的物件都是 Place 。

舉個例子,先建立四筆資料,兩筆是 Restaurant,兩筆是 Bar

bar1 = Bar.objects.create(name='bar001', bartender='foo')
bar2 = Bar.objects.create(name='bar002', bartender='foo2')
r1 = Restaurant.objects.create(name='r001', chef='chef001')
r2 = Restaurant.objects.create(name='r002', chef='chef002')

先用 Place.objects.select_subclasses() ,這樣會傳回

<InheritanceQuerySet [<Bar: Bar object (1)>, <Bar: Bar object (2)>, <Restaurant: Restaurant object (3)>, <Restaurant: Restaurant object (4)>]>

那用 Place.objects.select_subclasses(Bar) 以後,可以看到只有兩個 Bar,其他都是 Place

[<Bar: Bar object (1)>, <Bar: Bar object (2)>, <Place: Place object (3)>, <Place: Place object (4)>]

JoinManager

JoinManager ,從名稱看起來,是跟 Join 相關。實際上,使用 JoinManager 進行查詢時,它會依據目前的 model 來建立一個暫存表格,然後以這個暫存表格來做 Join。因此,可以利用它來提昇查詢速度。

不過根據我實驗的結果,至少,在 sqlite3 上,會卡到問題。我在猜想,是因為 JoinManager.join() 的程式裡直接使用 RAW SQL 的緣故。

QueryManager

QueryManager 做的事情主要是簡化篩選,以一般做篩選來說會這樣寫

qs = Place.objects.filter(name__startswith='Q').order_by(name)

那我們可以在宣告 model 時,加上 QueryManager:

from django.db import models
from model_utils.managers import InheritanceManager, QueryManager

class Place(models.Model):
    objects = InheritanceManager()
    name = models.CharField(max_length=256)

    name_startswith_q = QueryManager(name__startswith='Q').order_by(name)

使用的時候,就可以使用 name_startswith_q 來快速篩選

qs = Place.name_startswith_q.all()

SoftDeletableManager

軟刪除,這個 Manager 就是搭配 SoftDeletableModel 來使用的,SoftDeletableManager 負責的是查詢,它會排除掉 is_removed=True 的記錄。

結語

django-model-utils 的幾個 model ,像 TimeStampedModel、SoftDeletableModel 等等都很實用,可以自己少處理許多事情。

文章範例程式碼的網址:https://github.com/elleryq/ithome-iron-2020-django/tree/day-17


上一篇
16. djangorestframework-firebase
下一篇
18. django-crispy-forms
系列文
加速你的 Django 網站開發 - Django 的好用套件30

尚未有邦友留言

立即登入留言