1. 程式人生 > >Django ORM模型:想說愛你不容易

Django ORM模型:想說愛你不容易

作者:Vamei 出處:http://www.cnblogs.com/vamei 嚴禁轉載。

使用Python的Django模型的話,一般都會用它自帶的ORM(Object-relational mapping)模型。這個ORM模型的設計比較簡單,學起來不會特別花時間。不過,Django的ORM模型有自己的一套語法,有時候會覺得彆扭。這裡聊一下我自己的體會。

模型設計

這一部分算處理得比較好的部分。Django的資料模型的建立過程很簡單,就是繼承django.db.models中的Model類,然後給它增加屬性。每一個屬性可以對應關係資料庫中的一個欄位。比如在一個叫myapp的Django App下,建立models.py檔案:

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=10)

通過manage.py的makemigrations和migrate命令,就可以執行資料庫的遷移。上面的name屬性,就對應了生成的myapp_person表中名為"name"的一列。這裡的max_length=10對應了限制條件:

VARCHAR(10)

(在MySQL V4中,代表了10個位元組;在MySQL V5中,代表了10個字元。)

除了上面的字元型別,其他常見的欄位型別,在Django都有對應的*Field來表達,比如TextField、DateField、DateTimeField、IntegerField、DecimalField。此外,還有一些常見的限制條件,除了上面的max_length,還有default、unique、null、primary_key等等。數字型別的限制條件有max、min、max_digits、decimal_places。這些限制條件都通過引數的形式傳給屬性。有一些限制條件是Django提供的,並沒有資料庫層面的對應物,比如blank。

(當blank引數為真時,對應欄位可以為留為空白。)

在基本的模型設計上,Django ORM沒有留什麼坑。

關係

Django中的一對一、多對一、多對多關係可以通過下面方式表達:

from django.db import models


class Company(models.Model):
    name = models.CharField(max_length=10)


class Group(models.Model):
    name = models.CharField(max_length=10)


class Person(models.Model):
    name 
= models.CharField(max_length=10) class Customer(models.Model):
name = models.CharField(max_length=10) person
= models.OneToOneField(Person) company = models.ForeignKey(Company, on_delete=models.CASCADE) groups = models.ManyToManyField(Group)

Customer的定義中,用到一對一、多對一、多對多關係。它們分別通過OneToOneField、ForeignKey和ManyToManyField來實現。

需要注意的是,在Django ORM中,只能通過ForeignKey來定義多對一關係,不能顯示地定義一對多關係。但你可以使用模型物件的*_set語法來反向呼叫多對一關係。比如說:

company.customer_set   #company是一個Company的例項

就可以根據一對多關係,調到該公司下的所有客戶。此外,多對多關係也可以用類似的方式反向呼叫,比如:

group.customer_set

此外,你還可以在模型中加入related_name引數,從而在反省呼叫時,改用"*_set"之外的其他名稱,比如:

class Customer(models.Model):
   person  = models.OneToOneField(Person)
   address = models.CharField(max_length=100)
   company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="customers")

如果兩個模型之間有多個關係時,related_name可以防止*_set重名。

總的來說,上面的解決方案可以實現功能,並不影響使用。但我總是覺得這個解決方案有些醜陋。由於不能顯式地表達兩個模型之間的關係,模型之間的關係看起來不夠明瞭。特別是讀程式碼時,第一個類定義完全沒法提示一對多的關係。我必須要看到了第二個類定義,才能搞明白兩個模型之間的關係。真希望有一種顯式說明關係的辦法,降低讀程式碼時的認知負擔。

查詢

Django ORM可以通過一些方法來實現。其中的很多方法返回的是Django自定義的QuerySet類的迭代器。Python看到迭代器時會懶惰求值,所以這些方法返回時並不會真正進行資料庫操作。這樣,多個方法串聯操作時,就避免了重複操作資料庫。返回QuerySet的常見方法包括:

all()
filter()
exclude()
annotate()
order_by()
reverse()
distinct()
...

對於依賴具體資料的操作,QuerySet會求值。比如遍歷QuerySet時,就會先執行資料庫操作。用len()獲得QuerySet長度時,也會造成QuerySet估值。此外QuerySet一些方法,比get()、count()、earlist()、exists()等,都會對QuerySet進行求值。因此,在寫程式時,要注意QuerySet求值的時間點,避免重複的資料庫操作。

SQL的WHERE條件可以通過引數的形式來傳給方法。這些引數一般是"[欄位]__[運算子]"的命名方式,比如:

Customer.objects.filter(name__contains="abc")

除了contains,還有in、gt、lt、startswith、date、range等等操作符,能實現的WHERE條件確實夠全的了。

不過,這又是一個有點彆扭的地方,即通過命名方式來控制查詢行為。我看過有的ORM是用lambda的形式來表達WHERE條件,還有的會做一個類似於contains()的方法,都要比Django ORM的方式好看。如果是跨表查詢,Django的方式就更醜了:

Customer.objects.filter(company__name__contains="xxx")

無限的雙下劃線啊……

聚合

Django實現聚合的方式簡直是噩夢。貌似ORM對錶達GROUP BY很無力,原始碼裡的註釋就認輸了:

聚合的aggregate()和annotate()方法可以實現基本的功能,但稍微複雜一點,程式碼就變得魔幻了:

看到一大串values()、annotate()變來變去,有沒有覺得頭暈?我覺得這種情況下,可以直接上原始的SQL查詢語句了,沒必要再自己折騰自己。

F表示式和Q表示式

F表示式指代了一列,對於update操作時引用列的值有用。Q表示式代表了WHERE的一個條件,可以用於多個WHERE條件的連線。這些都是Django ORM用來彌補缺陷的。就拿Q表示式來說。查詢方法中跟多個引數的話,相當於多個WHERE條件。這些條件會預設為AND關係。為了表達OR和NOT關係,Django ORM就造了個Q表示式,比如:

filter(Q(name__contains="abc")|Q(name__startswith("xxx")))

為了彌補缺陷,Django ORM又增加了一種語法風格。於是,學習路上又多了一個坑……

總結

總的來說,Django ORM在實現基礎的資料庫操作方面沒問題。但如果需要構建複雜的SQL語句,與其在Django ORM裡繞來繞去,還不如直接用原始的SQL語句。這個是我最強烈的一個感受。當然,Django ORM還是可用的工具。我寫這篇文章的目的,是提醒大家不要誤把糟糕的設計當做精巧的語法。