Django ContentType 的使用
引入
一切優化,最終都是關於需求的優化。本文介紹需求確定之後的資料庫表結構設計優化。
程式設計師應該都知道,程式設計是資料結構和演算法的結合。所謂資料就是使用者需要訪問和操作的資源,比如購物類App裡面的商品,圖書、衣服、鞋帽等等。演算法就是我們通過一系列的獲取資料、過濾資料、彙總並編排資料並最終展現給使用者的一個過程。
演算法的實現複雜度非常重要,因為它直接關乎到使用者體驗,演算法實現簡單則使用者體驗好,反則使用者體驗差,而演算法的實現複雜度直接與資料的儲存結構相關,資料的儲存結構如果設計的非常好,那麼向用戶展示資料的演算法可能會非常簡單,反之,則將異常複雜。
因此,資料結構或者說,資料庫表結構的設計是至關重要的。
現在的應用程式,持久儲存資料採用的是關係資料庫,即Oracle,MySQL等等資料庫軟體,而使用者需要的資料,則是儲存到資料庫表中,不同的人,設計出來的表結構不同,導致演算法的實現過程會有非常明顯的差別。
概念
Django有一個用來跟蹤所以已安裝App的 models 的框架,名為 contenttypes 。
ContentType其實是Django原生App之一,也是 contenttypes 的實現基礎。
該應用提供了一種高階的、通用的介面用來管理和維護我們應用程式的 models 。
拿之前的DRF專案來舉例,該框架會為每一個APP的 models 建立對應的資訊,該資訊儲存在 content_type 表中,請看下圖:
如上圖所示,每一個App對應多個 model , 儲存在名為 django_content_type 的表中,該表只有 id , app_label 和 model 三個欄位。第一個欄位儲存的是 model 的序號,下文中, contenttypes 框架正是通過 model 的 id 找到該 model , app_label 儲存的是App的名稱,而 model 欄位則儲存的是每一個 model 的名稱。
安裝使用
使用 django-admin startproject 命令建立一個Django專案後,在 INSTALLED_APPS 列表中會出現該框架,如下圖所示:
什麼時候需要用到該框架
如果檢視官方文件,你只會發現一些官方的對於該框架的解釋,但是在什麼地方能夠發揮它的最大價值,官方文件中並沒有詳細說明,接下來,設計一個需求,並根據這個需求的優化,來一步步學習 contenttypes 框架。
一個簡單的需求
假設現在需要開發一個課程App,該app共有如下課程表:
共四個課程表:作業系統基礎,Python基礎,面向物件,Web框架。
需求
學生學完每個課程的最後一門課後,會獲得該門課程的優惠券和該課程的總優惠券,比如學生學完作業系統原理課程後,會獲得作業系統原理通關優惠券和作業系統基礎課程優惠券兩張優惠券。
第一次優化
看到如此多的優惠券表,作為程式設計師,這不能忍,要好好優化優化,不然,這就是給自己挖了一個巨大的坑,以後擴充套件和維護肯定會特別不方便。如何優化呢?
是否可以將如此多的優惠券表合成一張表呢?
這樣就實現了使用一張優惠券表來儲存所有的優惠券和對應的課程的關係資訊(使用了課程id),注意每個課程id字典都是該優惠券表的一個外來鍵欄位。
第二次優化
經過第一次優化,表數量減少了,但是對這樣的表進行增刪改查操作將會非常麻煩,經過優化,對這個表的增刪改查操作速度有了非常明顯的提升。
仔細想想,設計優惠券和課程之間的關係,無非就是為了增刪改查這些操作,只要能夠通過課程id找到它所對應的所有優惠券,或者通過優惠券能唯一定位到某一個具體的課程,這就是本質需求。
現在的問題是,課程 id 是重複的,每一個表裡面的 id 都是從1開始計數,而優惠券裡面的 id 如果僅僅只是儲存課程 id ,很顯然,就無法唯一定位到具體的課程。那麼,通過什麼方式來唯一定位 id 呢?
請看下圖的優化:
看到了嗎?使用的 table_id 和 table_row_id 來唯一定義某個課程表中的某一個課程,首先,必須通過某一個資訊定位到具體的表,然後再通過課程id來定位具體的課程。
那麼, table_id 從哪裡來呢?
上面的 django_content_type 表嗎?它裡面有三個欄位,其中id欄位儲存的就是整個Django專案中所有表的序號id。
優惠券表共有四個欄位, id 欄位, coupon_name 欄位儲存優惠券資訊, table_id 欄位儲存的是課程表的 id ,它應該是一個外來鍵,關聯到 django_content_type 表, table_row_id 欄位,該欄位的性質也是外來鍵,但是不能具體指向某一個表,因為,該欄位儲存的是所有表的 id 。
優化到這裡,差不多了,算是比較合適的表結構設計了。但是看起來還有點麻煩,特別是 table_row_id 欄位。不是特別好實現,下面就到 contenttypes 的使用了。
使用contenttypes建表
1.匯入模組
from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
2.建立模型類
class OperationSystem(models.Model): """ id course_name 1 "計算機基礎" 2 "計算機組成原理" 3 "作業系統原理" """ course_name = models.CharField(max_length=32) coupons = GenericRelation(to="Coupon") def __str__(self): return self.course_name class PythonBasic(models.Model): """ id course_name 1 "資料型別" 2 "字元編碼" 3 "檔案操作" """ course_name = models.CharField(max_length=32) coupons = GenericRelation(to="Coupon") def __str__(self): return self.course_name class Oop(models.Model): """ id course_name 1 "面向物件三大特性" 2 "元類" """ course_name = models.CharField(max_length=32) coupons = GenericRelation(to="Coupon") def __str__(self): return self.course_name class WebFramework(models.Model): """ id course_name 1 "web框架原理" 2 "ORM" """ course_name = models.CharField(max_length=32) coupons = GenericRelation(to="Coupon") def __str__(self): return self.course_name class Coupon(models.Model): """ id name content_type_id object_id 1 "作業系統優惠券" 9 2 2 "Python 優惠券" 9 2 """ coupon_name = models.CharField(max_length=32) content_type = models.ForeignKey(ContentType, verbose_name="關聯到django的ContentType表", on_delete=models.CASCADE) object_id = models.PositiveIntegerField(verbose_name="關聯表中的資料行ID") content_object = GenericForeignKey("content_type", "object_id") def __str__(self): return self.coupon_name
使用方式:
- 匯入模組一個是 ContentType 這個 model
- 另一個是 GenericForeignKey 和 GenericRelation
優惠券表包含 coupon_name , 該欄位是一個外來鍵,關聯到 ContentType 表;
還包含 object_id 欄位, 該欄位儲存課程id;
這兩個欄位的預設名稱分別為 content_type , object_id ;
另外最重要的是 content_object 欄位,它是 GenericForeignKey 這個類的例項化物件,建立這個物件時,需要把上面兩個欄位作為引數傳遞給它。
對於優惠券表的所有資料操作,只要是涉及到需要查詢優惠券對應的課程,或者通過課程查詢其對應的優惠券,都是通過 content_object 來進行的。
值得注意的是,該欄位並不真實存在表中, Coupon 表結構如下圖所示:
基本資料操作
先插入一些基礎資料,也就是第一張圖中展示的資料。
操作ContentType
上文提到, ContentType 是一個表,這個表提供一些方法以便進行資料操作:
>>> from django.contrib.contenttypes.models import ContentType >>> course_type = ContentType.objects.get(app_label="course", model="oop") >>> course_type Out[7]: <ContentType: oop> # 獲取類名:字串形式 >>> course_type.model Out[8]: 'oop' # 獲取model,之後可以通過objects.all來查詢資料 >>> course_type.model_class() Out[9]: course.models.Oop # 查詢資料 >>> course_type.get_object_for_this_type(course_name="元類") Out[10]: <Oop: 元類>
給課程字元編碼新增一個字元編碼通關優惠券:
新增資料
>>> from course.models import PythonBasic >>> pb_obj = PythonBasic.objects.get(id=2) >>> from course.models import Coupon >>> Coupon.objects.create(coupon_name="字元編碼通關優惠券", content_object=pb_obj) Out[15]: <Coupon: 字元編碼通關優惠券>
檢視資料是否新增成功:
可以看到,是第九張表的第二行資料,檢視 django_content_type 表發現,該課程所在的表 Python 基礎表,在整個專案中的確是第九張表,而且該課程在該表中是第二行資料, id 為2。
這樣,就可以通過表的 id 和資料 id 具體定位到某一個課程,然後給該課程繫結一個優惠券。當然還可以給它繫結多個優惠券。
>>> from course.models import Coupon >>> from course.models import PythonBasic >>> pb_obj = PythonBasic.objects.get(id=2) >>> Coupon.objects.create(coupon_name="字元編碼通關優惠券", content_object=pb_obj)
可以看到,定位具體的課程資訊是通過 content_object 這個並不存在 Coupon 表中的欄位來操作的。
刪除資料
刪除字元編碼通關優惠券對應的所有課程
>>> Coupon.objects.filter(coupon_name="字元編碼通關優惠券").delete()
或者
>>> pb_obj = PythonBasic.objects.get(id=2) >>> ob_obj.coupons.all().delete()
修改資料
>>> Coupon.objects.filter(coupon_name="字元編碼通關優惠券").update(coupon_name="編碼通關優惠券")
查詢資料
查詢面向物件通關優惠券綁定了那些課程
>>> coupon_obj = Coupon.objects.filter(coupon_name="面向物件通關優惠券").first() >>> coupon_obj.content_type # <PythonBasic: 字元編碼>
使用反向查詢欄位檢視字元編碼課程共有哪些優惠券
>>> pb_obj = PythonBasic.objects.get(id=2) >>> pb_obj.coupons.all() Out[35]: <QuerySet [<Coupon: 字元編碼通關優惠券>, <Coupon: 面向物件通關優惠券>]>
~>.&l