当模型查询API不够用时,您可以退回到编写原始SQL。 Django为您提供了两种执行原始SQL查询的方法:您可以使用Manager.raw()来执行原始查询并返回模型实例,也可以完全避免模型层并直接执行自定义SQL。
无论何时写入原始SQL,您都应该非常小心。 每次使用它时,都应该正确地转义用户可以通过使用参数来控制的参数,以防止SQL注入攻击。
执行原始查询
raw()管理器方法可用于执行返回模型实例的原始SQL查询:
Manager.raw(raw_query, params=None, translations=None)
此方法使用原始SQL查询,执行该查询,并返回一个django.db.models.query.RawQuerySet实例。 这个RawQuerySet实例可以像正常的QuerySet一样迭代来提供对象实例。 以一个例子来最好地说明这一点。 假设你有以下模型:
class Person(models.Model):
first_name = models.CharField(...)
last_name = models.CharField(...)
birth_date = models.DateField(...)
然后你可以像这样执行自定义SQL:
>>> for p in Person.objects.raw('SELECT * FROM myapp_person'):
... print(p)
John Smith
Jane Jones
当然,这个例子并不是很令人兴奋 - 和运行Person.objects.all()完全一样。 然而,raw()有一堆其他的选项,使它非常强大。
模型表名称
在上面的例子中,Person表的名字来自哪里? 默认情况下,Django通过将模型的“应用标签”(在manage.py startapp中使用的名称)连接到模型的类名称,并在它们之间加下划线来计算数据库表名。 在这个例子中,我们假设Person模型存在于名为myapp的应用程序中,所以它的表格将是myapp_person。
有关更多详细信息,请查阅db_table选项的文档,该选项还允许您手动设置数据库表名称。
在传递给raw()的SQL语句上没有进行检查。 Django期望这个语句将从数据库中返回一组行,但是没有执行这个操作。 如果查询没有返回行,则会导致(可能是神秘的)错误。
将查询字段映射到模型字段
raw()自动将查询中的字段映射到模型上的字段。 查询中字段的顺序无关紧要。 换句话说,以下两个查询的工作原理是相同的:
>>> Person.objects.raw('SELECT id, first_name, last_name, birth_date FROM myapp_perso\
n')
...
>>> Person.objects.raw('SELECT last_name, birth_date, first_name, id FROM myapp_perso\
n')
...
匹配是通过名称完成的。 这意味着您可以使用SQL的AS子句将查询中的字段映射到模型字段。 因此,如果您有其他具有Person数据的表,您可以轻松将其映射到Person实例中:
>>> Person.objects.raw('''SELECT first AS first_name,
... last AS last_name,
... bd AS birth_date,
... pk AS id,
... FROM some_other_table''')
只要名称匹配,模型实例将被正确创建。 或者,可以使用raw()的translations参数将查询中的字段映射到模型字段。 这是一个将查询中的字段名称映射到模型上的字段名称的字典。 例如,上面的查询也可以写成:
>>> 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()支持索引,所以如果你只需要第一个结果就可以编写:
>>> first_person = Person.objects.raw('SELECT * FROM myapp_person')[0]
但是,索引和切片不在数据库级别执行。 如果您的数据库中有大量的Person对象,那么在SQL级别限制查询会更高效:
>>> first_person = Person.objects.raw('SELECT * FROM myapp_person LIMIT 1')[0]
推迟模型字段
字段也可能被忽略:
>>> people = Person.objects.raw('SELECT id, first_name FROM myapp_person')
此查询返回的Person对象将是延迟模型实例(请参阅defer())。 这意味着查询中省略的字段将按需加载。 例如:
>>> 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
从外观看,这看起来像查询已经检索到名字和姓氏。 但是,这个例子实际上发出了3个查询。 只有第一个名字是由raw()查询检索的 - 最后一个名字在打印时都是按需要检索的。
只有一个字段不能忽略 - 主键字段。 Django使用主键来标识模型实例,因此它必须始终包含在原始查询中。 如果忘记包含主键,则会引发InvalidQuery异常。
添加注释
您还可以执行包含未在模型中定义的字段的查询。 例如,我们可以使用PostgreSQL的age()函数来获取由数据库计算其年龄的人员列表:
>>> 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.
...
将参数传递给raw()
如果您需要执行参数化查询,则可以将params参数传递给raw():
>>> lname = 'Doe'
>>> Person.objects.raw('SELECT * FROM myapp_person WHERE last_name = %s', [lname])
params是参数的列表或字典。 无论数据库引擎如何,您都可以在查询字符串中使用%s占位符,或者在字典中使用%(键)的占位符(当然,其中的密钥由字典密钥替换)。 这些占位符将被params参数中的参数替换。
不要在原始查询中使用字符串格式!将上面的查询写为:
'>>> query ='SELECT * FROM myapp_person WHERE last_name =%s'%lname
Person.objects.raw(query)
别这样做。
使用params参数完全可以保护您免受SQL注入攻击,这是攻击者在数据库中注入任意SQL的常见漏洞。 如果使用字符串插值,迟早会成为SQL注入的牺牲品。 只要你记得总是使用params参数,你就会受到保护。
直接执行自定义SQL
有时甚至是Manager.raw()还不够:您可能需要执行不完全映射到模型的查询,或者直接执行UPDATE,INSERT或DELETE查询。 在这些情况下,您可以直接访问数据库,完全绕过模型层。 对象django.db.connection表示默认的数据库连接。 要使用数据库连接,请调用connection.cursor()以获取游标对象。 然后,调用cursor.execute(sql,[params])来执行SQL,并使用cursor.fetchone()或cursor.fetchall()返回结果行。 例如:
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
请注意,如果要在查询中包含文字百分号,则在传递参数的情况下必须将它们加倍:
cursor.execute("SELECT foo FROM bar WHERE baz = '30%'")
cursor.execute("SELECT foo FROM bar WHERE baz = '30%%' AND
id = %s", [self.id])
如果使用多个数据库,则可以使用django.db.connections来获取特定数据库的连接(和游标)。 django.db.connections是一个类似于字典的对象,允许您使用其别名来检索特定的连接:
from django.db import connections
cursor = connections['my_db_alias'].cursor()
# Your code here...
默认情况下,Python DB API将返回没有字段名称的结果,这意味着最终会得到一个值列表,而不是字典。 在性能价格比较低的情况下,您可以使用类似下面的方式将结果作为字典返回:
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()
]
以下是两者之间差异的一个例子:
>>> 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}]
连接和游标
连接和游标大部分实现了PEP 249中描述的标准Python DB-API,除了事务处理。 如果您不熟悉Python DB-API,
请注意,cursor.execute()中的SQL语句使用占位符“%s”,而不是直接在SQL中添加参数。
如果使用这种技术,底层的数据库将根据需要自动转义您的参数。 还要注意,Django期望“%s”占位符,而不是?占位符,由SQLite Python绑定使用。 这是为了一致性和完整性。 使用游标作为上下文管理器:
with connection.cursor() as c:
c.execute(...)
相当于:
c = connection.cursor()
try:
c.execute(...)
finally:
c.close()
添加额外管理器方法
添加额外的Manager方法是将表级功能添加到模型的首选方法。
(对于行级功能 - 即作用于模型对象的单个实例的函数 - 使用Model方法,而不是自定义的Manager方法。)自定义的Manager方法可以返回任何您想要的。 它不必返回一个QuerySet。
例如,此自定义Manager提供了一个方法with_counts(),该方法返回所有OpinionPoll对象的列表,每个对象都有一个作为聚合查询结果的额外num_responses属性:
from django.db import models
class PollManager(models.Manager):
def with_counts(self):
from django.db import connection
cursor = connection.cursor()
cursor.execute("""
SELECT p.id, p.question, p.poll_date, COUNT(*)
FROM polls_opinionpoll p, polls_response r
WHERE p.id = r.poll_id
GROUP BY p.id, p.question, p.poll_date
ORDER BY p.poll_date DESC""")
result_list = []
for row in cursor.fetchall():
p = self.model(id=row[0], question=row[1], poll_date=row[2])
p.num_responses = row[3]
result_list.append(p)
return result_list
class OpinionPoll(models.Model):
question = models.CharField(max_length=200)
poll_date = models.DateField()
objects = PollManager()
class Response(models.Model):
poll = models.ForeignKey(OpinionPoll)
person_name = models.CharField(max_length=50)
response = models.TextField()
通过这个例子,你可以使用OpinionPoll.objects.with_counts()返回带有num_responses属性的OpinionPoll对象列表。 另外需要注意的是,这个例子中的Manager方法可以访问self.model来获取它们所附的模型类。
下一步是什么?
在下一章中,我们将向您展示Django的通用视图框架,该框架可以帮助您节省构建遵循常见模式的Web站点的时间。
网友评论