使用Python的Django模型的話,一般都會(huì)用它自帶的ORM(Object-relational mapping)模型。這個(gè)ORM模型的設(shè)計(jì)比較簡(jiǎn)單,學(xué)起來(lái)不會(huì)特別花時(shí)間。不過(guò),Django的ORM模型有自己的一套語(yǔ)法,有時(shí)候會(huì)覺(jué)得別扭。這里聊一下我自己的體會(huì)。

 

模型設(shè)計(jì)

這一部分算處理得比較好的部分。Django的數(shù)據(jù)模型的建立過(guò)程很簡(jiǎn)單,就是繼承django.db.models中的Model類,然后給它增加屬性。每一個(gè)屬性可以對(duì)應(yīng)關(guān)系數(shù)據(jù)庫(kù)中的一個(gè)字段。比如在一個(gè)叫myapp的Django App下,創(chuàng)建models.py文件:

from django.db import modelsclass Person(models.Model):
    name = models.CharField(max_length=10)

通過(guò)manage.py的makemigrations和migrate命令,就可以執(zhí)行數(shù)據(jù)庫(kù)的遷移。上面的name屬性,就對(duì)應(yīng)了生成的myapp_person表中名為"name"的一列。這里的max_length=10對(duì)應(yīng)了限制條件:

VARCHAR(10)

(在MySQL V4中,代表了10個(gè)字節(jié);在MySQL V5中,代表了10個(gè)字符。)

 

除了上面的字符類型,其他常見(jiàn)的字段類型,在Django都有對(duì)應(yīng)的*Field來(lái)表達(dá),比如TextField、DateField、DateTimeField、IntegerField、DecimalField。此外,還有一些常見(jiàn)的限制條件,除了上面的max_length,還有default、unique、null、primary_key等等。數(shù)字類型的限制條件有max、min、max_digits、decimal_places。這些限制條件都通過(guò)參數(shù)的形式傳給屬性。有一些限制條件是Django提供的,并沒(méi)有數(shù)據(jù)庫(kù)層面的對(duì)應(yīng)物,比如blank。

(當(dāng)blank參數(shù)為真時(shí),對(duì)應(yīng)字段可以為留為空白。)

 

在基本的模型設(shè)計(jì)上,Django ORM沒(méi)有留什么坑。

 

關(guān)系

Django中的一對(duì)一、多對(duì)一、多對(duì)多關(guān)系可以通過(guò)下面方式表達(dá):

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營(yíng)銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營(yíng)銷培訓(xùn)

from django.db import modelsclass 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)

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營(yíng)銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營(yíng)銷培訓(xùn)

Customer的定義中,用到一對(duì)一、多對(duì)一、多對(duì)多關(guān)系。它們分別通過(guò)OneToOneField、ForeignKey和ManyToManyField來(lái)實(shí)現(xiàn)。

 

需要注意的是,在Django ORM中,只能通過(guò)ForeignKey來(lái)定義多對(duì)一關(guān)系,不能顯示地定義一對(duì)多關(guān)系。但你可以使用模型對(duì)象的*_set語(yǔ)法來(lái)反向調(diào)用多對(duì)一關(guān)系。比如說(shuō):

company.customer_set   #company是一個(gè)Company的實(shí)例

 

就可以根據(jù)一對(duì)多關(guān)系,調(diào)到該公司下的所有客戶。此外,多對(duì)多關(guān)系也可以用類似的方式反向調(diào)用,比如:

group.customer_set

此外,你還可以在模型中加入related_name參數(shù),從而在反省調(diào)用時(shí),改用"*_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")

如果兩個(gè)模型之間有多個(gè)關(guān)系時(shí),related_name可以防止*_set重名。

 

總的來(lái)說(shuō),上面的解決方案可以實(shí)現(xiàn)功能,并不影響使用。但我總是覺(jué)得這個(gè)解決方案有些丑陋。由于不能顯式地表達(dá)兩個(gè)模型之間的關(guān)系,模型之間的關(guān)系看起來(lái)不夠明了。特別是讀代碼時(shí),第一個(gè)類定義完全沒(méi)法提示一對(duì)多的關(guān)系。我必須要看到了第二個(gè)類定義,才能搞明白兩個(gè)模型之間的關(guān)系。真希望有一種顯式說(shuō)明關(guān)系的辦法,降低讀代碼時(shí)的認(rèn)知負(fù)擔(dān)。

 

查詢

Django ORM可以通過(guò)一些方法來(lái)實(shí)現(xiàn)。其中的很多方法返回的是Django自定義的QuerySet類的迭代器。Python看到迭代器時(shí)會(huì)懶惰求值,所以這些方法返回時(shí)并不會(huì)真正進(jìn)行數(shù)據(jù)庫(kù)操作。這樣,多個(gè)方法串聯(lián)操作時(shí),就避免了重復(fù)操作數(shù)據(jù)庫(kù)。返回QuerySet的常見(jiàn)方法包括:

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營(yíng)銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營(yíng)銷培訓(xùn)

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

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營(yíng)銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營(yíng)銷培訓(xùn)

對(duì)于依賴具體數(shù)據(jù)的操作,QuerySet會(huì)求值。比如遍歷QuerySet時(shí),就會(huì)先執(zhí)行數(shù)據(jù)庫(kù)操作。用len()獲得QuerySet長(zhǎng)度時(shí),也會(huì)造成QuerySet估值。此外QuerySet一些方法,比get()、count()、earlist()、exists()等,都會(huì)對(duì)QuerySet進(jìn)行求值。因此,在寫(xiě)程序時(shí),要注意QuerySet求值的時(shí)間點(diǎn),避免重復(fù)的數(shù)據(jù)庫(kù)操作。

 

SQL的WHERE條件可以通過(guò)參數(shù)的形式來(lái)傳給方法。這些參數(shù)一般是"[字段]__[運(yùn)算符]"的命名方式,比如:

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

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

 

不過(guò),這又是一個(gè)有點(diǎn)別扭的地方,即通過(guò)命名方式來(lái)控制查詢行為。我看過(guò)有的ORM是用lambda的形式來(lái)表達(dá)WHERE條件,還有的會(huì)做一個(gè)類似于contains()的方法,都要比Django ORM的方式好看。如果是跨表查詢,Django的方式就更丑了:

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

無(wú)限的雙下劃線啊……

 

聚合

Django實(shí)現(xiàn)聚合的方式簡(jiǎn)直是噩夢(mèng)。貌似ORM對(duì)表達(dá)GROUP BY很無(wú)力,源代碼里的注釋就認(rèn)輸了:

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營(yíng)銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營(yíng)銷培訓(xùn)

聚合的aggregate()和annotate()方法可以實(shí)現(xiàn)基本的功能,但稍微復(fù)雜一點(diǎn),代碼就變得魔幻了:

seo優(yōu)化培訓(xùn),網(wǎng)絡(luò)推廣培訓(xùn),網(wǎng)絡(luò)營(yíng)銷培訓(xùn),SEM培訓(xùn),網(wǎng)絡(luò)優(yōu)化,在線營(yíng)銷培訓(xùn)

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

 

F表達(dá)式和Q表達(dá)式

F表達(dá)式指代了一列,對(duì)于update操作時(shí)引用列的值有用。Q表達(dá)式代表了WHERE的一個(gè)條件,可以用于多個(gè)WHERE條件的連接。這些都是Django ORM用來(lái)彌補(bǔ)缺陷的。就拿Q表達(dá)式來(lái)說(shuō)。查詢方法中跟多個(gè)參數(shù)的話,相當(dāng)于多個(gè)WHERE條件。這些條件會(huì)默認(rèn)為AND關(guān)系。為了表達(dá)OR和NOT關(guān)系,Django ORM就造了個(gè)Q表達(dá)式,比如:

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

為了彌補(bǔ)缺陷,Django ORM又增加了一種語(yǔ)法風(fēng)格。于是,學(xué)習(xí)路上又多了一個(gè)坑……

 

總結(jié)

總的來(lái)說(shuō),Django ORM在實(shí)現(xiàn)基礎(chǔ)的數(shù)據(jù)庫(kù)操作方面沒(méi)問(wèn)題。但如果需要構(gòu)建復(fù)雜的SQL語(yǔ)句,與其在Django ORM里繞來(lái)繞去,還不如直接用原始的SQL語(yǔ)句。這個(gè)是我最強(qiáng)烈的一個(gè)感受。