引入
一切優化,最終都是關於需求的優化。本文介紹需求確定之后的數據庫表結構設計優化。
程序員應該都知道,編程是數據結構和算法的結合。所謂數據就是用戶需要訪問和操作的資源,比如購物類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: 面向對象通關優惠券>]>
~>.<~