在模型查詢API不夠用的情況下,你可以使用原始的sql語(yǔ)句。django提供兩種方法使用原始sql進(jìn)行查詢:一種是使用Manager.raw()方法,進(jìn)行原始查詢并返回模型實(shí)例;另一種是完全避開模型層,直接執(zhí)行自定義的sql語(yǔ)句。
警告
編寫原始的sql語(yǔ)句時(shí),應(yīng)該格外小心。每次使用的時(shí)候,都要確保轉(zhuǎn)義了參數(shù)中的任何控制字符,以防受到sql注入攻擊。更多信息請(qǐng)參閱防止sql注入。
raw()方法用于原始的sql查詢,并返回模型的實(shí)例:
Manager.raw(raw_query, params=None, translations=None)
這個(gè)方法執(zhí)行原始的sql查詢之后,返回django.db.models.query.RawQuerySet的實(shí)例。RawQuerySet實(shí)例可以像一般的QuerySet那樣,通過(guò)迭代來(lái)提供對(duì)象的實(shí)例。
這里最好通過(guò)例子展示一下,假設(shè)存在以下模型:
class Person(models.Model):
first_name = models.CharField(...)
last_name = models.CharField(...)
birth_date = models.DateField(...)
你可以像這樣執(zhí)行自定義的sql語(yǔ)句:
>>> for p in Person.objects.raw('SELECT * FROM myapp_person'):
... print(p)
John Smith
Jane Jones
當(dāng)然,這個(gè)例子不是特別有趣,和直接使用Person.objects.all()的結(jié)果一模一樣。但是,raw()擁有其它更強(qiáng)大的使用方法。
模型表的名稱
在上面的例子中,Person表的名稱是從哪里得到的?
通常,Django通過(guò)將模型的名稱和模型的“應(yīng)用標(biāo)簽”(你在manage.py startapp中使用的名稱)進(jìn)行關(guān)聯(lián),用一條下劃線連接他們,來(lái)組合表的名稱。在這里我們假定Person模型存在于一個(gè)叫做myapp的應(yīng)用中,所以表就應(yīng)該叫做myapp_person。
更多細(xì)節(jié)請(qǐng)查看db_table選項(xiàng)的文檔,它也可以讓你自定義表的名稱。
警告
傳遞給raw()方法的sql語(yǔ)句并沒有任何檢查。django默認(rèn)它會(huì)返回一個(gè)數(shù)據(jù)集,但這不是強(qiáng)制性的。如果查詢的結(jié)果不是數(shù)據(jù)集,則會(huì)產(chǎn)生一個(gè)錯(cuò)誤。
警告
如果你在mysql上執(zhí)行查詢,注意在類型不一致的時(shí)候,mysql的靜默類型強(qiáng)制可能導(dǎo)致意想不到的結(jié)果發(fā)生。如果你在一個(gè)字符串類型的列上查詢一個(gè)整數(shù)類型的值,mysql會(huì)在比較前強(qiáng)制把每個(gè)值的類型轉(zhuǎn)成整數(shù)。例如,如果你的表中包含值'abc'和'def',你查詢'where mycolumn=0',那么兩行都會(huì)匹配。要防止這種情況,在查詢中使用值之前,要做好正確的類型轉(zhuǎn)換。
警告
雖然RawQuerySet可以像普通的QuerySet一樣迭代,RawQuerySet并沒有實(shí)現(xiàn)可以在QuerySet上使用的所有方法。例如,__bool__()和__len__()在RawQuerySet中沒有被定義,所以所有RawQuerySet轉(zhuǎn)化為布爾值的結(jié)果都是True。RawQuerySet中沒有實(shí)現(xiàn)他們的原因是,在沒有內(nèi)部緩存的情況下會(huì)導(dǎo)致性能下降,而且增加內(nèi)部緩存不向后兼容。
raw()方法自動(dòng)將查詢字段映射到模型字段。
字段的順序并不重要。換句話說(shuō),下面兩種查詢的作用相同:
>>> Person.objects.raw('SELECT id, first_name, last_name, birth_date FROM myapp_person')
...
>>> Person.objects.raw('SELECT last_name, birth_date, first_name, id FROM myapp_person')
...
Django會(huì)根據(jù)名字進(jìn)行匹配。這意味著你可以使用sql的as子句來(lái)映射二者。所以如果在其他的表中有一些Person數(shù)據(jù),你可以很容易地把它們映射成Person實(shí)例。
>>> Person.objects.raw('''SELECT first AS first_name,
... last AS last_name,
... bd AS birth_date,
... pk AS id,
... FROM some_other_table''')
只要名字能對(duì)應(yīng)上,模型的實(shí)例就會(huì)被正確創(chuàng)建。 又或者,你可以在raw()方法中使用翻譯參數(shù)。翻譯參數(shù)是一個(gè)字典,將表中的字段名稱映射為模型中的字段名稱、例如,上面的查詢可以寫成這樣:
>>> name_map = {'first': 'first_name', 'last': 'last_name', 'bd': 'birth_date', 'pk': 'id'}
>>> Person.objects.raw('SELECT * FROM some_other_table', translations=name_map)
raw()方法支持索引訪問(wèn),所以如果只需要第一條記錄,可以這樣寫:
>>> first_person = Person.objects.raw('SELECT * FROM myapp_person')[0]
然而,索引和切片并不在數(shù)據(jù)庫(kù)層面上進(jìn)行操作。如果數(shù)據(jù)庫(kù)中有很多的Person對(duì)象,更加高效的方法是在sql層面限制查詢中結(jié)果的數(shù)量:
>>> first_person = Person.objects.raw('SELECT * FROM myapp_person LIMIT 1')[0]
字段也可以被省略:
>>> people = Person.objects.raw('SELECT id, first_name FROM myapp_person')
查詢返回的Person對(duì)象是一個(gè)延遲的模型實(shí)例(請(qǐng)見 defer())。這意味著被省略的字段,在訪問(wèn)時(shí)才被加載。例如:
>>> for p in Person.objects.raw('SELECT id, first_name FROM myapp_person'):
... print(p.first_name, # This will be retrieved by the original query
... p.last_name) # This will be retrieved on demand
...
John Smith
Jane Jones
從表面上來(lái)看,看起來(lái)這個(gè)查詢獲取了first_name和last_name。然而,這個(gè)例子實(shí)際上執(zhí)行了3次查詢。只有first_name字段在raw()查詢中獲取,last_name字符按在執(zhí)行打印命令時(shí)才被獲取。
只有一種字段不可以被省略,就是主鍵。Django 使用主鍵來(lái)識(shí)別模型的實(shí)例,所以它在每次原始查詢中都必須包含。如果你忘記包含主鍵的話,會(huì)拋出一個(gè)InvalidQuery異常。
你也可以在查詢中包含模型中沒有定義的字段。例如,我們可以使用PostgreSQL的age()函數(shù)來(lái)獲得一群人的列表,帶有數(shù)據(jù)庫(kù)計(jì)算出的年齡。
>>> people = Person.objects.raw('SELECT *, age(birth_date) AS age FROM myapp_person')
>>> for p in people:
... print("%s is %s." % (p.first_name, p.age))
John is 37.
Jane is 42.
...
如果你需要參數(shù)化的查詢,可以向raw() 方法傳遞params參數(shù)。
>>> lname = 'Doe'
>>> Person.objects.raw('SELECT * FROM myapp_person WHERE last_name = %s', [lname])
params是存放參數(shù)的列表或字典。你可以在查詢語(yǔ)句中使用%s占位符,或者對(duì)于字典使用%(key)占位符(key會(huì)被替換成字典中鍵為key的值),無(wú)論你的數(shù)據(jù)庫(kù)引擎是什么。這樣的占位符會(huì)被替換成參數(shù)表中正確的參數(shù)。
注意
SQLite后端不支持字典,你必須以列表的形式傳遞參數(shù)。
警告
不要在原始查詢中使用字符串格式化!
它類似于這種樣子:
>> query = 'SELECT * FROM myapp_person WHERE last_name = %s' % lname >> Person.objects.raw(query)
使用參數(shù)化查詢可以完全防止sql注入,一種普遍的漏洞使攻擊者可以向你的數(shù)據(jù)庫(kù)中注入任何sql語(yǔ)句。如果你使用字符串格式化,早晚會(huì)受到sql輸入的攻擊。只要你記住默認(rèn)使用參數(shù)化查詢,就可以免于攻擊。
有時(shí)Manager.raw()方法并不十分好用,你不需要將查詢結(jié)果映射成模型,或者你需要執(zhí)行UPDATE、INSERT以及DELETE查詢。
在這些情況下,你可以直接訪問(wèn)數(shù)據(jù)庫(kù),完全避開模型層。
django.db.connection對(duì)象提供了常規(guī)數(shù)據(jù)庫(kù)連接的方式。為了使用數(shù)據(jù)庫(kù)連接,調(diào)用connection.cursor()方法來(lái)獲取一個(gè)游標(biāo)對(duì)象之后,調(diào)用cursor.execute(sql, [params])來(lái)執(zhí)行sql語(yǔ)句,調(diào)用cursor.fetchone()或者curser.fetchall()來(lái)返回結(jié)果行。
例如:
from django.db import connection
def my_custom_sql(self):
cursor = connection.cursor()
cursor.execute("UPDATE bar SET foo = 1 WHERE baz = %s", [self.baz])
cursor.execute("SELECT foo FROM bar WHERE baz = %s", [self.baz])
row = cursor.fetchone()
return row
注意如果你的查詢中包含百分號(hào)字符,你需要寫成兩個(gè)百分號(hào)字符,以便能正確傳遞參數(shù):
cursor.execute("SELECT foo FROM bar WHERE baz = '30%'")
cursor.execute("SELECT foo FROM bar WHERE baz = '30%%' AND id = %s", [self.id])
如果你使用了不止一個(gè)數(shù)據(jù)庫(kù),你可以使用django.db.connections來(lái)獲取針對(duì)特定數(shù)據(jù)庫(kù)的連接(以及游標(biāo))對(duì)象。django.db.connections是一個(gè)類似于字典的對(duì)象,允許你通過(guò)它的別名獲取特定的連接
from django.db import connections
cursor = connections['my_db_alias'].cursor()
# Your code here...
通常,Python DB API會(huì)返回不帶字段的結(jié)果,這意味著你需要以一個(gè)列表結(jié)束,而不是一個(gè)字典?;ㄙM(fèi)一點(diǎn)性能之后,你可以返回一個(gè)字典形式的結(jié)果,像這樣:
def dictfetchall(cursor):
"Returns all rows from a cursor as a dict"
desc = cursor.description
return [
dict(zip([col[0] for col in desc], row))
for row in cursor.fetchall()
]
下面是一個(gè)體現(xiàn)二者區(qū)別的例子:
>>> cursor.execute("SELECT id, parent_id FROM test LIMIT 2");
>>> cursor.fetchall()
((54360982L, None), (54360880L, None))
>>> cursor.execute("SELECT id, parent_id FROM test LIMIT 2");
>>> dictfetchall(cursor)
[{'parent_id': None, 'id': 54360982L}, {'parent_id': None, 'id': 54360880L}]
連接和游標(biāo)主要實(shí)現(xiàn)PEP 249中描述的Python DB API標(biāo)準(zhǔn),除非它涉及到事務(wù)處理。
如果你不熟悉Python DB-API,注意cursor.execute()中的sql語(yǔ)句使用占位符"%s",而不是直接在sql中添加參數(shù)。如果你使用它,下面的數(shù)據(jù)庫(kù)會(huì)在必要時(shí)自動(dòng)轉(zhuǎn)義你的參數(shù)。
也要注意Django使用"%s"占位符,而不是SQLite Python綁定的"?"占位符。這是一致性和可用性的緣故。
Django 1.7中的改變。
PEP 249并沒有說(shuō)明游標(biāo)是否可以作為上下文管理器使用。在python2.7之前,游標(biāo)可以用作上下文管理器,由于魔術(shù)方法lookups中意想不到的行為(Python ticket #9220)。Django 1.7 顯式添加了對(duì)允許游標(biāo)作為上下文管理器使用的支持。
將游標(biāo)作為上下文管理器使用:
with connection.cursor() as c:
c.execute(...)
等價(jià)于:
c = connection.cursor()
try:
c.execute(...)
finally:
c.close()