Django 入门指南(五)
八、Django 模型查询和管理器
正如您在前一章中所了解到的,Django 模型通过类封装数据,执行数据验证,用于与 Django 项目的关系数据库进行交互,并且有无数的选项来保证和定制数据在 Django 项目中的操作方式。
在这一章中,我们将建立在前面的 Django 模型概念上,并学习 Django 模型查询和管理器。我们将从深入研究 Django 模型 CRUD(创建-读取-更新-删除)操作开始,包括单个、多个和关系查询,涵盖它们的速度和效率含义。接下来,您将了解 Django 模型支持的许多 SQL 查询变体,包括生成 SQL WHERE 语句的字段查找;建模方法以产生 SQL 语句,如 DISTINCT 和 ORDER 以及执行 SQL 聚合操作、数据库函数和子查询的查询表达式。
接下来,您将学习当 Django 的内置 SQL 工具不够用时,如何创建原始(开放式)SQL 查询。最后,您将学习如何在 Django 模型中创建和配置定制模型管理器。
Django 模型中的 CRUD 单一记录
处理单一记录是 Django 模型中最常见的任务之一。接下来,我将把下面的部分组织成传统的 web 应用 CRUD 操作,并描述每种情况下的各种技术,这样您就可以更好地掌握在不同情况下使用什么。
请注意,尽管以下部分集中于实际的 CRUD 操作及其行为,但有时我会不可避免地在示例中引入更高级的查询概念(例如,字段查找),这将在本章的后面部分详细描述。
使用 save()或 Create()创建一条记录
要在 Django 模型上创建单个记录,您只需要创建一个模型实例,并在其上调用save()方法。清单 8-1 展示了为名为Store的模型创建单个记录的过程。
Tip
参考本书附带的源代码来运行练习,以减少打字和自动访问测试数据。
# Import Django model class
from coffeehouse.stores.models import Store
# Create a model Store instance
store_corporate = Store(name='Corporate',address='624 Broadway',state='CA',email='corporate@coffeehouse.com')
# Assign attribute value to instance with Python dotted notation
store_corporate.city = 'San Diego'
# Invoke the save() method to create the record
store_corporate.save() # If successful, record reference has id
store_corporate.id
Listing 8-1.Create a single record with model save() method
正如您在清单 8-1 中看到的,您可以在一个单独的步骤中声明所有的实例属性,或者您可以使用 Python 的点符号在引用本身上逐个分配属性值。一旦实例准备就绪,调用它的save()方法在数据库中创建记录。当您调用save()方法时,有两个重要的行为需要注意:
- 默认情况下,所有 Django 模型都被分配了一个名为
id的自动递增主键,这个主键是在您启动模型的数据库表时创建的——请参阅上一章的“Django 模型和迁移工作流”一节了解更多详细信息。这意味着数据库给一个记录分配一个id值——除非你明确地给实例提供一个id值——这个值被传递回引用。 - 如果记录的创建违反了 Django 模型创建的任何数据库或 Django 验证规则,则记录的创建将被拒绝。这意味着如果一个新实例不符合这些验证规则中的任何一个,
save()就会生成一个错误。关于规则验证的更多细节,请参见上一章的“Django 模型数据类型”一节。
当您使用save()方法创建记录时,这是最重要的两点。关于与 Django 模型save()方法相关的全套选项和微妙之处,请参见上一章的表格 7-3 和“模型方法”部分
在成功调用清单 8-1 中的save()方法后,您可以看到对象引用被赋予了id属性——由数据库创建——该属性用于将它直接链接到一个数据库记录,该记录稍后可以被更新和/或删除。
create()方法为创造记录提供了一条更短的路线。清单 8-2 展示了使用create()方法在清单 8-1 中创建的等价记录。
# Import Django model class
from coffeehouse.stores.models import Store
# Create a model Store instance which is saved automatically
store_corporate = Store.objects.create(name='Corporate',address='624 Broadway',city='San Diego',state='CA',email='corporate@coffeehouse') # If successful, record reference has id
store_corporate.id
Listing 8-2.Create a single record with create() method
您可以在清单 8-2 中看到,create()方法是通过模型的默认objects模型管理器在 Django 模型类上调用的。create()方法接受代表模型实例字段值的参数。create()的执行返回一个对已创建记录的对象引用,包括一个id值,就像save()方法一样。
在幕后,create()方法实际上使用了相同的save()方法,但是它使用模型管理器来允许在一行中创建一个记录。
用 get()或 get_or_create()读取单个记录
要读取单个数据库记录,您可以使用get()方法——它是模型的默认objects模型管理器的一部分——它接受任何模型字段来限定记录。清单 8-3 展示了get() Django 模型方法的一个基本例子。
# Import Django model class
from coffeehouse.stores.models import Store
# Get the store with the name "Downtown" or equivalent SQL: 'SELECT....WHERE name = "Downtown"
downtown_store = Store.objects.get(name="Downtown")
# Define uptown_email for the query
uptown_email = "uptown@coffeehouse.com"
# Get the store with the email value uptown_email or equivalent SQL: 'SELECT....WHERE email = "uptown@coffeehouse.com"'
uptown_email_store = Store.objects.get(email=uptown_email)
# Once the get() method runs, you can access an object's attributes
# either in logging statements, functions or templates
downtown_store.address
downtown_store.email
# Note you can access the object without attributes.
# If the Django model has a __str__/ method definition, the output is based on this method
# If the Django model has no __str__ method definition, the output is just <object>
print(uptown_email_store)
Listing 8-3.Read model record with get() method
正如您在清单 8-3 中看到的,get()方法使用 Django 模型属性作为其参数来检索特定的记录。第一个例子用name=Downtown获取Store记录,第二个例子用email=uptown@coffeehouse.com获取Store记录。一旦记录被分配给一个变量,您就可以使用 Python 的点符号来访问它的内容或属性。
Tip
除了单个字段-name="Downtown "或 email = " uptown @…"-get()方法还接受多个字段来生成 and 查询(例如,get(email="uptown@…",name = " Downtown ")来获取电子邮件和姓名都匹配的记录)。此外,Django 还提供了字段查找来创建更好的单记录查询(例如,get(name__contains="Downtown ")来生成子串查询)。请参阅“按 SQL 关键字分类的查询”一章的后面部分。
使用 Django 模型的get()方法就是这么简单。然而,get()方法有一些您应该知道的行为:
- 使用
get(),查询必须匹配且只能匹配一条记录。如果没有匹配的记录,您将得到一个<model>.DoesNotExist错误。如果有多个匹配记录,你将得到一个MultipleObjectsReturned错误。 - 每次调用都会立即命中数据库。这意味着 Django 不会对相同或多个调用进行缓存。
知道了这些get()限制,让我们来探索如何处理第一个场景,它涉及一个不存在的记录。当试图读取一个不存在的记录时,一个常见的情况是获取它,如果它不存在,就创建它。清单 8-4 展示了如何使用get_or_create()方法来达到这个目的。
# Import Django model class
from coffeehouse.items.models import Menu
# Get or create a menu instance with name="Breakfast"
menu_target, created = Menu.objects.get_or_create(name="Breakfast")
Listing 8-4.Read or create model record with get_or_create() method
正如您在清单 8-4 中看到的,get_or_create()方法——也是模型的默认objects模型管理器的一部分——在 Django 模型类上被调用,使用模型的属性作为它的参数来一步获得或创建记录。get_or_create()方法返回一对结果,模型实例——无论是创建的还是读取的——以及一个指示模型实例是创建的还是读取的布尔值(即,如果创建了就返回True,如果读取了就返回False)。
get_or_create()方法是一种同时使用了get()和create()方法的快捷方式——正如您在上一节中了解到的,后者在幕后使用了save()方法。不同之处在于,当get()没有找到匹配时,get_or_create()方法会自动处理错误情况。清单 8-5 展示了get_or_create()方法是如何在幕后工作的,如果您喜欢显式处理get()错误方法,也可以使用它。
from django.core.exceptions import ObjectDoesNotExist
from coffeehouse.items.models import Menu
try:
menu_target = Menu.objects.get(name="Dinner")
# If get() throws an error you need to handle it.
# You can use either the generic ObjectDoesNotExist or
# <model>.DoesNotExist which inherits from
# django.core.exceptions.ObjectDoesNotExist, so you can target multiple
# DoesNotExist exceptions
except Menu.DoesNotExist: # or the generic "except ObjectDoesNotExist:"
menu_target = Menu(name="Dinner")
menu_target.save()
Listing 8-5.Replicate get_or_create() method with explicit try/except block and save method
正如您在清单 8-5 中看到的,当您知道某个记录可能不存在,并且您无论如何都想创建它时,有必要编写更多的代码(例如,错误处理、获取和保存调用)。因此在这种情况下,get_or_create()方法成为了一种有用的捷径。
现在让我们看看第二个get()限制,它涉及到在一个查询中获得多个记录。按照设计,如果有多个记录匹配一个查询,get()方法会抛出一个MultipleObjectsReturned错误。这种行为是一个实际的特性,因为在某些情况下,您希望确保查询只返回一条记录,否则会得到通知(例如,对于用户或产品的查询,重复项被认为是错误的)。
如果查询有可能返回一个或多个记录,那么您需要放弃使用get()方法,而使用模型管理器的filter()或exclude()方法。filter()或exclude()方法都产生一个名为QuerySet的多记录数据结构,可以通过一个额外的QuerySet方法将其简化为一个记录(例如,Item.objects.filter(name__contains='Salad').first()获得名称包含沙拉子串的第一个Item记录)。
由于 Django 模型的filter()或exclude()方法是为多记录查询设计的,这些方法和QuerySet行为将在后面关于多记录 CRUD 操作的章节中详细描述。类似于first()的其他QuerySet方法也将在后面关于通过 SQL 关键字分类的模型查询的章节中描述。
用 save()、Update()、update_or_create()或 refresh_from_db()更新单个记录
如果您已经有了一个对模型记录的引用,那么更新就像使用 Python 的点符号更新其属性并对其调用save()方法一样简单。清单 8-6 说明了这个过程。
# Import Django model class
from coffeehouse.stores.models import Store
# Get the store with the name "Downtown" or equivalent SQL: 'SELECT....WHERE name = "Downtown"
downtown_store = Store.objects.get(name="Downtown")
# Update the name value
downtown_store.name = "Downtown (Madison)"
# Call save() with the update_fields arg and a list of record fields to update selectively
downtown_store.save(update_fields=['name'])
# Or you can call save() without any argument and all record fields are updated
downtown_store.save()
Listing 8-6.Update model record with the save() method
在清单 8-6 中,您可以看到save()方法以两种方式被调用。您可以使用带有字段列表的update_fields参数来更新某些字段,并在大型模型中获得性能提升。或者另一种选择是使用没有任何参数的save(),在这种情况下,Django 更新所有字段。
如果您还没有对要更新的记录的引用,那么首先获取它(即发出一个 SELECT 查询)然后用save()方法更新它会稍微有些低效。此外,在单独的步骤中执行更新过程会导致争用情况。例如,如果另一个用户在同一时间获取相同的数据,并且也进行了更新,那么你们都会争着保存它,但是谁的更新是最终的,谁的更新会被覆盖呢?因为任何一方都不知道另一方正在处理相同的数据,所以您需要一种方法来指示(技术上称为锁定或隔离)数据,以避免出现竞争情况。
对于这种情况,您可以使用update()方法——模型的默认objects模型管理器的一部分——它在单个操作中执行更新,并保证没有竞争条件。清单 8-7 说明了这个过程。
from coffeehouse.stores.models import Store
Store.objects.filter(id=1).update(name="Downtown (Madison)")
from coffeehouse.items.models import Item
from django.db.models import F
Item.objects.filter(id=3).update(stock=F('stock') +100)
Listing 8-7.Update model record with the update() method
清单 8-7 中的第一个例子使用update()方法用id=1更新Store记录,并将其name设置为Downtown (Madison)。清单 8-7 中的第二个例子使用 Django F表达式和update()方法用id=3更新Item记录,并将其stock值设置为当前股票值加 100。目前,不要担心 Django F表达式——稍后将在更详细的查询中描述它们——只需认识到 Django F表达式允许您在查询中引用模型字段——作为 SQL 表达式——这在这种情况下是在单个操作中执行更新所必需的。
Caution
如果不小心的话,update()方法可以跨多个记录更新一个字段。update()方法前面是 objects.filter()方法,它可以返回多条记录的查询结果。注意,在清单 8-7 中,查询使用 id 字段来定义查询,确保只有一条记录与查询匹配,因为 id 是表的主键。如果 objects.filter()中的查询定义使用不太严格的查找(例如,字符串),您可能会无意中更新比预期更多的记录。
与上一节描述的便利get_or_create()方法相似,Django 也提供了便利update_or_create()方法。当您想要执行更新并且不确定记录是否存在时,这种方法很有用。清单 8-8 说明了这个过程。
# Import Django model class
from coffeehouse.stores.models import Store
values_to_update = {'email':'downtown@coffeehouse.com'}
# Update for record with name='Downtown' and city='San Diego' is found, otherwise create record
obj_store, created = Store.objects.update_or_create(
name='Downtown',city='San Diego', defaults=values_to_update)
Listing 8-8.Update or create model record with the update_or_create() method
清单 8-8 中做的第一件事是创建一个包含要更新的字段值的字典。接下来,您向update_or_create传递一个查询参数,用于一个期望的对象(即您希望更新或创建的对象),以及包含要更新的字段值的字典。
对于清单 8-8 中的情况,如果已经有一个带有name='Downtown'和city='San Diego'的Store记录,则更新values_to_update中记录的值,如果没有匹配的Store,则记录一个带有name='Downtown'、city='San Diego'以及values_to_update中值的新商店记录。update_or_create方法返回一个更新的或创建的对象,以及一个布尔值来指示记录是否是新创建的。
Note
update_or_create 仅适用于单个记录的查询。如果有多个记录与 update_or_create()中的查询匹配,您将得到错误 MultipleObjectsReturned,就像 get()方法一样。
如果您不小心更改了一个模型记录,您可以使用refresh_from_db()方法从数据库中恢复它的数据,如清单 8-9 所示。
from coffeehouse.stores.models import Store
store_corporate = Store.objects.get(id=1)
store_corporate.name = 'Not sure about this name'
# Update from db again
store_corporate.refresh_from_db() # Model record name now reflects value in database again
store_corporate.name
# Multiple edits
store_corporate.name = 'New store name'
store_corporate.email = 'newemail@coffeehouse.com' store_corporate.address = 'To be confirmed'
# Update from db again, but only address field
# so store name and email remain with local values
store_corporate.refresh_from_db(fields=['address'])
Listing 8-9.Update model record from database with the refresh_from_db() method
正如您在清单 8-9 中看到的,在更改了模型记录上的name字段值之后,您可以调用引用上的refresh_from_db()方法来更新数据库中的模型记录。清单 8-9 中的第二个例子使用了带有fields参数的refresh_from_db()方法,它告诉 Django 只更新在fields列表中声明的模型字段,允许对其他字段的任何(本地)编辑保持不变。
使用 Delete()删除单个记录
如果您已经有一个对记录的引用,删除它就像对它调用delete()方法一样简单。清单 8-10 说明了这个过程。
# Import Django model class
from coffeehouse.stores.models import Store
# Get the store with the name "Downtown" or equivalent SQL: 'SELECT....WHERE name = "Downtown"
downtown_store = Store.objects.get(name="Downtown")
# Call delete() to delete the record in the database
downtown_store.delete()
Listing 8-10.Delete model record with the delete() method
对于您还没有想要删除的记录的引用的情况,首先获取它(例如,发出一个 SELECT 查询)然后用delete()方法删除它可能会稍微有些低效。对于这种情况,您可以使用delete()方法并将其附加到一个查询中,这样一切都在一个操作中完成。清单 8-11 说明了这个过程。
from coffeehouse.items.models import Menu
Menu.objects.filter(id=1).delete()
Listing 8-11.Delete model record with the delete() method
on query
不管您使用的delete()方法是直接引用还是通过objects模型管理器,delete()方法总是返回一个包含删除操作结果的字典。例如,如果 8-11 中的删除操作成功,它将返回(1, {'items.Menu': 1}),表明删除了一条items.Menu类型的记录。如果 8-10 中的删除操作成功,它返回(5, {'stores.Store_amenities': 4, 'stores.Store': 1}),表明总共删除了五条记录,其中四条是stores.Store_amenities类型的,一条是stores.Store类型的——在这种情况下,删除了多条记录,因为stores.Store_amenities是Store模型中的一个模型关系。
Caution
如果不小心的话,delete()方法可以删除多条记录。delete()方法前面是 objects.filter()方法,该方法可以返回包含多条记录的查询结果。注意,在清单 8-11 中,查询使用 id 字段来定义查询,确保只有一条记录与查询匹配,因为 id 是表的主键。如果 objects.filter()中的查询定义使用不太严格的查找(例如,字符串),您可能会无意中删除比预期更多的记录。
Django 模型中的 CRUD 多个记录
在本节中,您将学习如何在 Django 模型中处理多个记录。虽然这个过程与处理单个记录一样简单,但是处理多个记录可能需要多次数据库调用、缓存技术和批量操作,所有这些都需要考虑在内,以最大限度地减少执行时间。
用 bulk_create()创建多条记录
要基于 Django 模型创建多个记录,可以使用内置的bulk_create()方法。bulk_create()方法的优点是它在一个查询中创建所有的条目,所以如果您有一个想要创建的十几个或一百个条目的列表,这将是非常有效的。清单 8-12 展示了为Store模型创建多个记录的过程。
# Import Django model class
from coffeehouse.stores.models import Store
# Create model Store instances
store_corporate = Store(name='Corporate',address='624 Broadway',city ='San Diego',state='CA',email='corporate@coffeehouse.com')
store_downtown = Store(name='Downtown',address='Horton Plaza',city ='San Diego',state='CA',email='downtown@coffeehouse.com')
store_uptown = Store(name='Uptown',address='240 University Ave',city ='San Diego',state='CA',email='uptown@coffeehouse.com')
store_midtown = Store(name='Midtown',address='784 W Washington St',city ='San Diego',state='CA',email='midtown@coffeehouse.com')
# Create store list
store_list = [store_corporate,store_downtown,store_uptown,store_midtown]
# Call bulk_create to create records in a single call
Store.objects.bulk_create(store_list)
Listing 8-12.Create multiple records of a Django model with the bulk_create() method
在清单 8-12 中,您可以看到bulk_create()方法接受一个模型实例列表,在一个步骤中创建所有记录。尽管bulk_create()方法很有效,但您应该知道它有一定的局限性:
- 它不支持保存前和保存后模型信号。-为了加快速度,与创建单个记录的
save()方法不同,bulk_create()方法不执行预保存和后保存模型信号。如果您不熟悉模型信号的概念,预保存和后保存模型信号允许在保存模型记录之前和之后执行自定义逻辑,这是上一章中讨论的主题。 - 它不支持跨多个表的模型(即,彼此之间有关系)。-因为记录是批量创建的,所以无法获得第一种类型的已创建记录的主键引用,这些记录随后用于创建相关的子记录。如果一个模型跨越多个表,那么您必须使用
save()方法单独创建每个记录,该方法支持创建跨越多个表的记录。
如果您面临bulk_create()方法的这些限制,唯一的替代方法是循环遍历每个记录并使用save()方法创建每个条目,如清单 8-13 所示。
# Same store_list as Listing 8-12
# Loop over each store and invoke save() on each entry # save() method called on each list member to create record
for store in store_list:
store.save()
Listing 8-13.Create multiple records with the save() method
正如我在介绍bulk_create()方法时提到的,如果对几十或几百条记录执行清单 8-13 中的过程会非常低效,但有时这是批量创建多条记录的唯一选择。但是,如果您手动处理模型事务,与清单 8-13 相关的速度问题可以得到改善。
清单 8-14 展示了如何使用save()方法并将整个记录创建过程分组到一个事务中,以加速批量创建过程。
# Import Django model and transaction class
from coffeehouse.stores.models import Store
from django.db import transaction
# Create store list, with same references from Listing 8-12
first_store_list = [store_corporate,store_downtown]
second_store_list = [store_uptown,store_midtown]
# Trigger atomic transaction so loop is executed in a single transaction
with transaction.atomic():
# Loop over each store and invoke save() on each entry
for store in first_store_list:
# save() method called on each member to create record
store.save()
# Method decorated with @transaction.atomic to ensure logic is executed in single transaction
@transaction.atomic
def bulk_store_creator(store_list):
# Loop over each store and invoke save() on each entry
for store in store_list:
# save() method called on each member to create record
store.save()
# Call bulk_store_creator with Store list
bulk_store_creator(second_store_list)
Listing 8-14.Create multiple records with save() method in a single transaction
正如您在清单 8-14 中看到的,有两种方法可以在单个数据库事务中创建批量操作,都使用django.db.transaction包。第一个实例使用with transaction.atomic():语句,因此该语句中的任何嵌套代码都在单个事务中运行。第二个实例使用@transaction.atomic方法装饰器,它确保方法操作在单个事务中运行。
Be Careful with Explicit Transactions
Django 的默认数据库事务机制为每个查询创建事务是有原因的:为了安全起见,尽量减少数据丢失的可能性。
如果您决定使用显式事务来提高性能——如清单 8-14 所示——请注意,要么创建所有记录,要么不创建记录。虽然这可能是一种期望的行为,但在某些情况下,它可能会导致意想不到的结果。确保您理解事务对您正在处理的数据的影响。前一章包含了更详细地讨论 Django 模型事务的部分。
用 all()、filter()、exclude()或 in_bulk()读取多条记录
要读取与 Django 模型相关的多个记录,可以使用几种方法,包括all()、filter()、exclude()和in_bulk()。all()方法的目的应该是不言自明的:它检索给定模型的所有记录。filter()方法用于将查询结果限制在给定的模型属性上,例如,filter(state='CA')是一个获取所有带有state='CA'的模型记录的查询。而exclude()方法用于执行排除给定模型属性上的记录的查询,例如exclude(state='AZ')是获取除state='AZ'之外的所有模型记录的查询。
还可以链接filter()和exclude()方法来创建更复杂的多记录查询。例如,filter(state='CA').exclude(city='San Diego')是一个获取所有带有state='CA'的模型记录并排除那些带有city='San Diego'的模型记录的查询。清单 8-15 展示了更多的多记录查询示例。
# Import Django model class
from coffeehouse.stores.models import Store
# Query with all() method or equivalent SQL: 'SELECT * FROM ...'
all_stores = Store.objects.all()
# Query with include() method or equivalent SQL: 'SELECT....WHERE city = "San Diego"'
san_diego_stores = Store.objects.filter(city='San Diego')
# Query with exclude() method or equivalent SQL: 'SELECT....WHERE NOT (city = "San Diego")'
non_san_diego_stores = Store.objects.exclude(city='San Diego')
# Query with include() and exclude() methods or equivalent SQL: 'SELECT....WHERE STATE='CA' AND NOT (city = "San Diego")'
ca_stores_without_san_diego = Store.objects.filter(state='CA').exclude(city='San Diego')
Listing 8-15.Read multiple records with with all(), filter(), and exclude() methods
Append .Query to View the Actual SQL
有时候,查看 Django 模型查询执行的实际 SQL 是有帮助的,甚至是必要的。您可以通过追加。查询转换为查询,如下面的清单所示:
from coffeehouse.stores.models import Store
import logging
stdlogger = logging.getLogger(__name__)
# Get the Store records with city San Diego
san_diego_stores = Store.objects.filter(city='San Diego')
stdlogger.debug("Query %s" % str(san_diego_stores.query))
# You can also use print(san_diego_stores.query)
正如您在前面的代码片段中看到的,您可以将 SQL 查询输出到 Python logger,或者使用“quick & dirty”print 语句。请注意。query 只适用于输出 QuerySets 的查询,所以它不像 get()方法那样适用于查询——稍后将详细介绍 QuerySets。第五章描述了检查模型查询使用的 SQL 的其他替代方法(如 Django 调试工具栏),第三章展示了如何在 Django 模板中输出 SQL 查询(如调试上下文处理器 sql_queries 变量)。
Tip
除了单个字段-city = " San Diego "或 state="CA "--all()、filter()和 exclude()方法还可以接受多个字段来生成 and 查询(例如,filter(city="San Diego ",state = " CA ")以获取城市和州匹配的记录)。请参阅“按 SQL 关键字分类的查询”一章的后面部分。
除了all()、filter()和exclude()方法,Django 模型还支持in_bulk()方法。in_bulk()方法旨在高效地读取许多记录,就像bulk_create()方法——在上一节中描述过——用于高效地创建许多记录。
与all()、filter()和exclude()方法相比,in_bulk()方法读取许多记录更有效,因为所有后一种方法都产生一个 QuerySet,而前者产生一个标准的 Python 字典。清单 8-16 展示了in_bulk()方法的使用。
# Import Django model class
from coffeehouse.stores.models import Store
# Query with in_bulk() all
Store.objects.in_bulk()
# Outputs: {1: <Store: Corporate (San Diego,CA)>, 2: <Store: Downtown (San Diego,CA)>, 3: <Store: Uptown (San Diego,CA)>, 4: <Store: Midtown (San Diego,CA)>}
# Compare in_bulk query to all() that produces QuerySet
Store.objects.all()
# Outputs: <QuerySet [<Store: Corporate (San Diego,CA)>, <Store: Downtown (San Diego,CA)>, <Store: Uptown (San Diego,CA)>, <Store: Midtown (San Diego,CA)>]>
# Query to get single Store by id
Store.objects.in_bulk([1])
# Outputs: {1: <Store: Corporate (San Diego,CA)>}
# Query to get multiple Stores by id
Store.objects.in_bulk([2,3])
# Outputs: {2: <Store: Downtown (San Diego,CA)>, 3: <Store: Uptown (San Diego,CA)>}
Listing 8-16.Read multiple records with with in_bulk() method
清单 8-16 中的第一个例子使用不带任何参数的in_bulk()方法来生成一个字典,该字典将存储模型的记录(即,就像all()方法一样)。但是,请注意in_bulk()方法的输出是一个标准的 Python 字典,其中每个键对应于记录的id值。
清单 8-16 中剩余的例子说明了in_bulk()方法如何接受一个值列表来指定应该从数据库中读取哪个记录 id。再次注意,虽然行为类似于filter()或exclude()方法,但是输出是标准的 Python 字典而不是QuerySet数据结构。
既然您已经清楚地了解了可以读取多个模型记录的各种方法,以及一些方法如何产生一个QuerySet而另一些方法不产生,这就引出了一个问题,什么是QuerySet,为什么首先要使用它?因此,在我们进入这一更广泛的部分的下一部分——如何对多个记录进行 CRUD 操作——之前,我们将绕一小段路来探索一下QuerySet数据类型。
理解查询集:惰性计算和缓存
QuerySet数据类型的第一个重要特征在技术上被称为惰性评估。这意味着QuerySet不会立即对数据库执行,它只是等待,直到被评估。换句话说,运行像Store.objects.all()这样的代码片段的行为不会马上涉及到任何数据库活动。清单 8-17 展示了如何在不触发数据库活动的情况下,一个接一个地进行链式查询。
# Import Django model class
from coffeehouse.stores.models import Store
# Query with all() method
stores = Store.objects.all()
# Chain filter() method on query
stores = stores.filter(state='CA')
# Chain exclude() method on query
stores = stores.exclude(city='San Diego')
Listing 8-17.Chained model methods to illustrate concept of QuerySet lazy evaluation
注意清单 8-17 中链接all()、filter()和exclude()方法的三个不同的语句。虽然清单 8-17 调用了三次数据库来获取带有state='CA'的Store记录,并排除了带有city='San Diego'的记录,但是没有任何数据库活动!
这就是QuerySet数据结构的工作原理。那么,对数据类型QuerySet的查询什么时候到达数据库呢?有许多触发器使QuerySet评估并调用实际的数据库调用。表 8-1 说明了各种触发器。
表 8-1。
Django QuerySet evaluation triggers that invoke an actual database call
| 评估触发器 | 描述 | 例子 | | --- | --- | --- | | 循环 | 在 QuerySet 上创建循环会触发数据库调用。 | 对于 Store.objects.all()中的 store: | | 使用“step”参数切片 | 用第三个参数(也称为“step”或“stride”参数)分割 QuerySet 会触发数据库调用。注意:用 1 或 2 个参数分割 Queryset 只会创建另一个 QuerySet。 | #每第 5 条记录的列表,用于前 100 条记录 Store.objects.all()[:100:5] #这不会触发数据库命中,(2 个参数)#记录 50 到 100 Store.objects.all()[49:99] #从第 6 条 Store.objects.all()[5:] #这不会触发数据库命中(1 个参数)Store.objects.all()[0] #第一条记录 | | 酸洗* | 酸洗一个查询集会强制在酸洗之前将所有结果加载到内存中。 | import pickle stores = store . objects . all()pickle _ stores = pickle . dumps(stores) | | repr()方法 | 对 QuerySet 调用 repr()会触发数据库调用。注意:这是为了在 Python 交互式解释器中方便起见,所以您可以立即看到查询结果。 | repr(Store.objects.all()) | | len()方法 | 对 QuerySet 调用 len()会触发数据库调用。注意:如果您只需要记录的数量,使用 Django model count()方法会更有效。 | total _ stores = len(store . objects . all())#注意:count()方法可以更有效地获得总计数 efficient _ total _ stores = store . objects . count() | | list()方法 | 对 QuerySet 调用 list()会触发数据库调用。 | store _ list = list(store . objects . all()) | | 布尔测试(bool()、or 和 or if 语句) | 对 QuerySet 进行布尔测试会触发数据库调用。注意:如果您只想检查记录是否存在,使用 Django model exists()方法会更有效。 | #如果 store . objects . filter(city='San Diego '),则检查是否有 city = ' San Diego '的商店:#在' San Diego' pass 中有一个商店#注意:exists()方法对于布尔值检查更有效 San _ Diego _ stores = store . objects . exists(city = ' San Diego ') |- Pickling is Python’s standard mechanism for object serialization, a process that converts a Python object into a character stream. The character stream contains all the information necessary to reconstruct the object at a later time. Pickling in the context of Django queries is typically used for heavyweight queries in an attempt to save resources (e.g., make a heavyweight query, pickle it, and on subsequent occasions consult the pickled query). You can consider pickling Django queries a rudimentary form of caching.
既然您已经知道了导致QuerySet调用数据库的触发器,那么让我们来看看另一个重要的QuerySet主题:缓存。
每个QuerySet都包含一个缓存以最小化数据库访问。第一次对一个QuerySet进行评估并进行数据库查询——参见表 8-1 中的评估触发器——Django 将结果保存在QuerySet的缓存中以备后用。
当应用经常需要使用相同的数据时,QuerySet的缓存是最有用的,因为它减少了对数据库的访问。然而,利用QuerySet的缓存伴随着一些与QuerySet的评估相关的微妙之处。一个经验法则是首先评估一个您计划多次使用的QuerySet,然后使用它的数据来利用QuerySet缓存。清单 8-18 中给出的例子最好地解释了这一点。
# Import Django model class
from coffeehouse.stores.models import Store
# CACHE USING SEQUENCE
# Query awaiting evaluation
lazy_stores = Store.objects.all()
# Iteration triggers evaluation and hits database
store_emails = [store.email for store in lazy_stores]
# Uses QuerySet cache from lazy_stores, since lazy_stores is evaluated in previous line
store_names = [store.name for store in lazy_stores]
# NON-CACHE SEQUENCE
# Iteration triggers evaluation and hits database
heavy_store_emails = [store.email for store in Store.objects.all()]
# Iteration triggers evaluation and hits database again, because it uses another QuerySet ref
heavy_store_names = [store.name for store in Store.objects.all()]
# CACHE USING SEQUENCE
# Query wrapped as list() for immediate evaluation
stores = list(Store.objects.all())
# Uses QuerySet cache from stores
first_store = stores[0]
# Uses QuerySet cache from stores
second_store = stores[1]
# Uses QuerySet cache from stores, set() is just used to eliminate duplicates
store_states = set([store.state for store in stores])
# Uses QuerySet cache from stores, set() is just used to eliminate duplicates
store_cities = set([store.city for store in stores])
# NON-CACHE SEQUENCE
# Query awaiting evaluation
all_stores = Store.objects.all()
# list() triggers evaluation and hits database
store_one = list(all_stores[0:1])
# list() triggers evaluation and hits database again, because partially evaluating a QuerySet does not populate the cache
store_one_again = list(all_stores[0:1])
# CACHE USING SEQUENCE
# Query awaiting evaluation
coffee_stores = Store.objects.all()
# Iteration triggers evaluation and hits database
[store for store in coffee_stores]
# Uses QuerySet cache from coffee_stores, because it's evaluated fully in previous line
store_1 = coffee_stores[0]
# Uses QuerySet cache from coffee_stores, because it's already evaluated in full
store_1_again = coffee_stores[0]
Listing 8-18.QuerySet caching behavior
正如您在清单 8-18 中看到的,利用QuerySet的缓存的序列会立即触发对QuerySet的评估,然后使用对评估后的QuerySet的引用来访问缓存的数据。不使用QuerySet缓存的序列要么不断地创建相同的QuerySet语句,要么使每个数据赋值的评估过程延迟。
不符合前面行为的缓存QuerySet的唯一边缘情况是清单 8-18 中的倒数第二个例子。如果通过切片触发了对QuerySet的部分求值(如[0]或[1:5]),缓存不会被填充。因此,为了确保使用了一个QuerySet缓存,您必须评估一个QuerySet,然后对结果进行切片,如清单 8-18 中的最后一个示例所示。
读取性能方法:defer()、only()、values()、values_list()、iterator()、exists()和 none()
虽然数据结构通过集成惰性评估和缓存机制向处理多个数据记录迈进了一步,但它们并没有涵盖处理大型数据查询所需的全部性能。
大型数据查询面临的一个常见性能问题是读取不必要的记录字段。尽管在大多数情况下,有选择地从数据库记录中读取哪些字段可能是事后才想到的,但是对于在具有多个字段的 Django 模型上进行的查询来说,这可能会产生重要的影响。
读取模型记录时提高性能的第一种方法是defer()和only()方法,这两种方法都是为了限定在查询中读取哪些字段。defer()和only()方法分别接受要延迟或加载的字段列表,根据您想要实现的目标,它们是互补的。例如,如果您想延迟加载大部分的模型字段,用only()指定加载哪些字段会更简单,如果您想延迟加载模型中的一个或几个字段,您可以在defer()方法中指定字段。清单 8-19 展示了defer()和only()方法的使用。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
# Item names on the breakfast menu
breakfast_items = Item.objects.filter(menu__name='Breakfast').only('name')
# All Store records with no email
all_stores = Store.objects.defer('email').all()
# Confirm loaded fields on overall query
breakfast_items.query.get_loaded_field_names()
# Outputs: {<class 'coffeehouse.items.models.Item'>: {'id', 'name'}}
all_stores.query.get_loaded_field_names()
# Outputs: {<class 'coffeehouse.stores.models.Store'>: {'id', 'address', 'state', 'city', 'name'}}
# Confirm deferred fields on individual model records breakfast_items[0].get_deferred_fields()
# Outputs: {'calories', 'stock', 'price', 'menu_id', 'size', 'description'}
all_stores[1].get_deferred_fields()
# Outputs: {'email'}
# Access deferred fields, note each call on a deferred field implies a database hit
breakfast_items[0].price
breakfast_items[0].size
all_stores[1].email
Listing 8-19.Read performance with defer() and only() to selectively read record fields
正如您在清单 8-19 中看到的,defer()和only()方法可以在查询的开始或结束链接到一个模型管理器(即objects),也可以与其他方法如all()和filter()结合使用。此外,请注意这两种方法如何接受要延迟或加载的字段列表。
为了验证哪些模型字段已经被延迟或加载,清单 8-19 展示了两种选择。第一种技术包括调用查询语句的查询引用上的get_loaded_field_names()来获取已加载字段的列表。第二种技术包括在一个模型实例上调用get_deferred_fields()方法来获得一个延迟字段列表。
那么,如何获得延迟字段呢?简单,你打电话给他们。在清单 8-18 的末尾,请注意尽管breakfast_items表示一个只加载name字段的查询,但还是调用了一个函数来获取price和size字段的值。类似地,清单 8-19 中的all_stores引用表示推迟email字段的查询;然而,您可以通过调用它来获取记录的email字段值。虽然最后一种技术需要额外的数据库命中来获得延迟的字段,但是它也说明了即使它们被延迟,获得记录的整个字段是多么容易。
values()和values_list()方法提供了另一种方法来分隔查询获取的字段。与产生模型实例的QuerySet的defer()和only()方法不同,values()和values_list()方法产生由普通字典、元组或列表组成的QuerySet实例。这具有不创建成熟模型实例的性能优势,尽管这也具有不能访问成熟模型实例的缺点。
values()和values_list()方法接受字段列表作为查询的一部分进行加载,这个过程如清单 8-20 所示。
Tip
您可以使用不带任何字段参数的 values()和 values_list()方法,以普通字典、元组或列表的形式生成完整的模型记录。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
# Item names on the breakfast menu
breakfast_items = Item.objects.filter(menu__name='Breakfast').values('name')
print(breakfast_items)
# Outputs: <QuerySet [{'name': 'Whole-Grain Oatmeal'}, {'name': 'Bacon, Egg & Cheese Biscuit'}]>
# All Store records with no email
all_stores = Store.objects.values_list('email','name','city').all()
print(all_stores)
# Outputs: <QuerySet [('corporate@coffeehouse.com', 'Corporate', 'San Diego'), ('downtown@coffeehouse.com', 'Downtown', 'San Diego'), ('uptown@coffeehouse.com', 'Uptown', 'San Diego'), ('midtown@coffeehouse.com', 'Midtown', 'San Diego')]>
all_stores_flat = Store.objects.values_list('email',flat=True).all()
print(all_stores_flat)
# Outputs: <QuerySet ['corporate@coffeehouse.com', 'downtown@coffeehouse.com', 'midtown@coffeehouse.com', 'uptown@coffeehouse.com']>
# It isn't possible to access undeclared model fields with values() and values_list()
breakfast_items[0].price #ERROR
# Outputs AttributeError: 'dict' object has no attribute 'price'
Listing 8-20.Read performance with values() and values_list() to selectively read record fields
清单 8-20 中的第一个变体生成了一个带有name字段的条目QuerySet,正如您所看到的,它生成了一个只有name字段和值的字典列表。接下来,使用values_list()方法查询所有Store模型的email、name和city字段。注意,与values()方法不同,values_list()方法以元组的形式产生了一个更紧凑的结构。在清单 8-20 中,您还可以看到values_list()方法接受可选的flat=True参数来将结果元组展平为一个普通列表。
最后,在清单 8-20 的末尾,您可以看到,当使用values()和values_list()方法时,不可能像使用defer()和only()方法那样,仅仅通过调用它们来获得未声明的字段。这种行为是由于values()和values_list()方法产生的淡化QuerySet,它们不是成熟的模型对象。
iterator()方法是 Django 模型中另一个可用的选项,它在QuerySet的结果上创建一个迭代器。iterator()方法对于打算使用一次的大型查询是理想的,因为这降低了存储数据所需的内存,这是所有 Python 迭代器的固有属性。清单 8-21 展示了一个使用iterator()方法的查询,附录 A 描述了 Python 迭代器背后的核心概念。
from coffeehouse.stores.models import Store
# All Store with iterator()
stores_on_iterator = Store.objects.all().iterator()
print(stores_on_iterator)
# Outputs: <generator object __iter__ at 0x7f2864db8fc0>
# Advance through iterator with __next__()
stores_on_iterator.__next__()
# Outputs: <Store: Corporate (San Diego,CA)>
stores_on_iterator.__next__()
# Outputs: <Store: Downtown (San Diego,CA)>
# Check if Store object with id=5 exists
Store.objects.filter(id=5).exists()
# Outputs: False
# Create empty QuerySet on Store model
Store.objects.none()
# Outputs: <QuerySet []>
Listing 8-21.Read performance with iterator(), exists(), and none()
另一种 Django 模型读取性能技术是exists()方法,如清单 8-21 所示,用于验证查询是否返回数据。虽然exists()方法对数据库执行查询,但是与标准查询相比,exists()使用的查询是一个简化版本,此外exists()方法返回一个布尔值 True 或 False,而不是一个完整的 QuerySet。这使得exists()方法成为对条件进行操作的查询的好选择,在这种情况下,只需要验证模型记录是否存在,而实际的记录数据是不必要的。
最后,Django 模型none()方法——在清单 8-21 的末尾说明——用于生成一个空的QuerySet,特别是一个名为EmptyQuerySet的子类。none()方法对于您故意需要分配一个空模型QuerySet的情况很有帮助,例如与 Django 模型表单或 Django 模板相关的边缘情况,它们以某种方式期望一个QuerySet实例。在这种情况下,有必要创建一个哑元QuerySet,而不是低效地创建返回数据的QuerySet并删除其内容。
正如您在本小节中了解到的,除了QuerySet数据结构,Django 还提供了许多专门设计的方法来有效地读取与 Django 模型相关的大量或少量记录。
Tip
请记住,上一节中的 in_bulk()方法也提供了优于基本的 all()、filter()和 exclude()方法的读取性能。
用 Update()或 select_for_update()更新多条记录
在单记录 CRUD 操作一节中,您探索了如何用update()方法更新单个记录;同样的方法可以处理多个记录的更新。清单 8-22 说明了这一过程。
from coffeehouse.stores.models import Store
Store.objects.all().update(email="contact@coffeehouse.com")
from coffeehouse.items.models import Item
from django.db.models import F
Item.objects.all().update(stock=F('stock') +100)
Listing 8-22.Update multiple records with the update() method
清单 8-22 中的第一个例子使用update()方法更新所有的Store记录,并将它们的 email 值设置为contact@coffeehouse.com。第二个例子使用 Django F表达式和update()方法来更新所有Drink记录,并将它们的stock值设置为当前股票值加 100。Django F表达式允许您在一个查询中引用模型字段,这对于在单个操作中执行更新是必要的。
虽然update()方法保证所有事情都在一个操作中完成以避免竞争情况,但是在某些情况下update()方法可能不足以完成复杂的更新。提供另一种更新多条记录的方法是select_for_update()方法,它锁定给定查询中的行,直到更新被标记为完成。清单 8-23 展示了select_for_update()方法的一个例子。
Select_For_Update() Support is Database Dependent
实际上,Django select_for_update()方法是基于 SQL 的 SELECT…FOR UPDATE 语法,并非所有数据库都支持该语法。Postgres、Oracle 和 MySQL 数据库支持此功能,但 SQLite 不支持。
此外,还有一个特殊的参数 nowait(例如,select_for_update(nowait=True)使查询不阻塞)。默认情况下,如果另一个事务获取了某个选定行的锁,select_for_update()查询将一直阻塞,直到锁被释放。如果使用 nowait,这将允许查询立即运行,如果另一个事务已经获取了冲突锁,则在计算 QuerySet 时会引发 DatabaseError。但是要注意,MySQL 不支持 nowait 参数,如果和 MySQL 一起使用,Django 会抛出 DatabaseError。
# Import Django model class
from coffeehouse.stores.models import Store
from django.db import transaction
# Trigger atomic transaction so loop is executed in a single transaction
with transaction.atomic():
store_list = Store.objects.select_for_update().filter(state='CA')
# Loop over each store to update and invoke save() on each entry
for store in store_list:
# Add complex update logic here for each store
# save() method called on each member to update
store.save()
# Method decorated with @transaction.atomic to ensure logic is executed in single transaction
@transaction.atomic
def bulk_store_updae(store_list):
store_list = Store.objects.select_for_update().exclude(state='CA')
# Loop over each store and invoke save() on each entry
for store in store_list:
# Add complex update logic here for each store
# save() method called on each member to update
store.save()
# Call bulk_store_update to update store records
bulk_store_update(store_list_to_update)
Listing 8-23.Update multiple records with a Django model with the select_for_update() method
清单 8-23 显示了select_for_update()的两种变体,一种使用显式事务,另一种修饰方法以在事务内限定其范围。两种变体使用相同的逻辑;他们首先用select_for_update()创建一个查询,然后遍历结果来更新每条记录,并使用save()来更新单个记录。通过这种方式,查询所涉及的行保持锁定,直到事务完成。
请注意,在使用select_for_update()时,绝对有必要使用清单 8-23 中描述的任何技术来使用事务。如果在支持的数据库中运行select_for_update()方法,并且没有使用清单 8-23 中所示的事务——保持 Django 的默认自动提交模式——Django 会抛出一个TransactionManagementError错误,因为行不能作为一个组被锁定。在不支持的数据库中使用select_for_update方法没有任何效果(也就是说,您不会看到错误)。
用 Delete()删除多条记录
要删除多条记录,可以使用delete()方法,并将其附加到查询中。清单 8-24 说明了这个过程。
from coffeehouse.stores.models import Store
Store.objects.filter(city='San Diego').delete()
Listing 8-24.Delete model records with the delete() method
清单 8-24 中的例子使用delete()方法删除带有city='San Diego'的Store记录。
跨 Django 模型的 CRUD 关系记录
在前一章中,您学习了 Django 模型关系如何通过特殊的模型数据类型(例如,ForgeignKey、ManyToManyField和OneToOne)帮助您改进数据维护。在 Django 模型关系上进行的 CRUD 操作也有一个特殊的语法。
尽管前面章节中的相同语法适用于对 Django 模型关系数据类型进行的直接操作,但是您也可以对定义关系数据类型的 Django 模型进行相反的操作。
Note
直接 Django 模型操作通过一个 Manager 类来操作,反向操作 Django 模型操作通过一个 RelatedManager 类来完成。
一对多 CRUD 操作
通过ForgeignKey模型数据类型建立一对多的关系。清单 8-25 显示了两个模型之间的一对多关系,包括对相关模型的一系列直接查询操作。
class Menu(models.Model):
name = models.CharField(max_length=30)
class Item(models.Model):
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
# Get the Menu of a given Item
Item.objects.get(name='Whole-Grain Oatmeal').menu.id
# Get the Menu id of a given Item
Item.objects.get(name='Whole-Grain Oatmeal').menu.name
# Get Item elements that belong to the Menu with name 'Drinks'
Item.objects.filter(menu__name='Drinks')
Listing 8-25.One to many ForeignKey direct query read operations
在清单 8-25 中,您可以看到Item模型声明了与Menu模型的ForeignKey关系。一旦一个Item模型以这种方式与一个Menu模型相关联,就有可能使用 Python 的点符号访问一个Menu模型,如清单 8-25 所示(例如,menu.id和menu.name获得Item引用上相关Menu实例的id和name)。请注意,在清单 8-25 中,还可以创建一个查询,使用__(两个下划线)(也称为“跟随符号”)来指示相关模型中的一个字段,从而引用相关模型。
清单 8-25 中的操作使用与非关系模型相同的查询语法,因为这些操作是与具有关系数据类型的模型分开创建的。然而,Django 也支持在没有关系数据类型的模型上启动的 CRUD 操作。
清单 8-26 展示了通过Menu模型的实例对其相关的Item模型进行的一系列 CRUD 操作。这些任务被称为反向操作,因为保持关系的模型——ForeignKey——是反向实现的。
from coffeehouse.items.models import Menu, Item
breakfast_menu = Menu.objects.get(name='Breakfast')
# Fetch all Item records for the Menu
breakfast_menu.item_set.all()
# Get the total Item count for the Menu
breakfast_menu.item_set.count()
# Fetch Item records that match a filter for the Menu
breakfast_menu.item_set.filter(name__startswith='Whole')
Listing 8-26.One to many ForeignKey reverse query read operations with _set syntax
清单 8-26 从一个针对Menu记录的标准 Django 查询开始。尽管Menu模型缺少与Item模型的显式关系,但是Item模型声明了与Menu模型的关系,Django 使用<one_model>.<many_model>_set语法创建了一个反向访问模式。
因此,从一个Menu记录中,您可以看到使用menu_record.item_set.all()语法可以获得与一个Menu记录有关系的所有Item记录。类似地,如清单 8-26 中的最后一个示例所示,可以生成一个查询,使用相同的_set语法过滤一组从Menu记录中分离出来的Item记录。
Tip
您可以使用 related_name 和 related_query_name 模型字段选项将 _set 语法更改为更明确的名称,或者完全禁用此行为。请参阅上一章“反转关系选项”小节中的“关系模型数据类型的选项”一节
正如反向的_set语法用于执行从没有显式关系字段的模型到有关系字段的模型的读取操作,也可以使用相同的_set语法来执行其他数据库操作(例如,创建、更新、删除),如清单 8-27 所示。
from coffeehouse.items.models import Menu, Item
breakfast_menu = Menu.objects.get(name='Breakfast')
# Create an Item directly on the Menu
# NOTE: Django also supports the get_or_create() and update_or_create() operations
breakfast_menu.item_set.create(name='Bacon, Egg & Cheese Biscuit',description='A fresh buttermilk biscuit...',calories=450)
# Create an Item separately and then add it to the Menu
new_menu_item = Item(name='Grilled Cheese',description='Flat bread or whole wheat ...',calories=500)
# Add item to menu using add()
# NOTE: bulk=False is necessary for new_menu_item to be saved by the Item model manager first
# it isn't possible to call new_menu_item.save() directly because it lacks a menu instance
breakfast_menu.item_set.add(new_menu_item,bulk=False)
# Create copy of breakfast items for later
breakfast_items = [bi for bi in breakfast_menu.item_set.all()]
# Clear menu references from Item elements (i.e. reset the Item elements menu field to null)
# NOTE: This requires the ForeignKey definition to have null=True
# (e.g. models.ForeignKey(Menu, null=True)) so the key is allowed to be turned null
# otherwise the error 'RelatedManager' object has no attribute 'clear' is thrown
breakfast_menu.item_set.clear()
# Verify Item count is now 0
breakfast_menu.item_set.count()
0
# Reassign Item set from copy of breakfast items
breakfast_menu.item_set.set(breakfast_items)
# Verify Item count is now back to original count
breakfast_menu.item_set.count()
3
# Clear menu reference from single Item element (i.e. reset an Item element menu field to null)
# NOTE: This requires the ForeignKey definition to have null=True
# (e.g. models.ForeignKey(Menu, null=True)) so the key is allowed to be turned null
# otherwise the error 'RelatedManager' object has no attribute 'remove' is thrown
item_grilled_cheese = Item.objects.get(name='Grilled Cheese')
breakfast_menu.item_set.remove(item_grilled_cheese)
# Delete the Menu element along with its associated Item elements
# NOTE: This requires the ForeignKey definition to have blank=True and on_delete=models.CASCADE (e.g. models.ForeignKey(Menu, blank=True, on_delete=models.CASCADE))
breakfast_menu.delete()
Listing 8-27.One to many ForeignKey reverse query create, update, delete operations with _set syntax
在清单 8-27 中,您可以看到在获得对Menu记录的引用后,您可以使用create()方法直接在_set引用上生成一个Item记录。清单 8-27 还展示了如何首先生成一个Item记录,然后使用同样适用于_set引用的add()方法将它链接到一个Menu记录。
Note
add()、create()、remove()、clear()和 set()关系方法都会立即对所有类型的相关字段应用数据库更改。这意味着不需要在关系的任何一端调用 save()。
接下来,清单 8-27 是clear()关系方法的一个例子。clear()方法用于分离关系,在清单 8-27 的情况下,它将与名为'Breakfast'到NULL的Menu相关联的所有Item记录的Menu引用设置为NULL(即,它不删除任何数据,只是删除关系引用)。值得一提的是,为了调用clear()方法,必须用null=True选项声明一个模型字段,以便将关系引用设置为NULL。
清单 8-27 中的add()关系方法用于关联一个关系的实例列表。在清单 8-27 的情况下,它恢复了由同一清单中的clear()方法产生的逻辑。add()关系方法的一个重要方面是,它在幕后使用模型的标准update()方法来添加关系,这反过来要求在创建关系之前预先保存两个模型记录。您可以通过使用清单 8-27 中使用的bulk=False来绕过这一限制,将保存操作委托给相关的管理器,并在不事先保存相关对象的情况下创建关系。
remove()关系方法的工作方式类似于clear()关系方法,但是它被设计成以一种精细的方式分离关系。在清单 8-27 的情况下,remove()方法将名为'Grilled Cheese'的Item记录的Menu引用设置为NULL(也就是说,它不会删除任何数据,只是删除关系引用)。类似于clear()关系方法,必须用null=True选项声明模型字段,以便将关系引用设置为NULL。
最后,清单 8-27 展示了在一个有关系的模型实例上调用delete()方法是如何删除调用它的实例及其相关的模型实例的。在清单 8-27 的情况下,breakfast_menu.delete()删除名为'Breakfast'的Menu和所有链接到它的Item实例。与clear()和remove()关系方法类似,delete()关系方法需要使用on_delete=models.CASCADE选项声明模型字段,以便自动删除相关模型。
Tip
有关其他 on_delete 选项,请参阅上一章“数据完整性选项”小节中的“关系模型数据类型选项”一节。
多对多 CRUD 操作
与一对多关系类似,多对多关系也支持直接和反向 CRUD 操作。清单 8-28 显示了两个模型之间的多对多关系,包括对相关模型的一系列直接查询操作。
class Amenity(models.Model):
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30,unique=True)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
email = models.EmailField()
amenities = models.ManyToManyField(Amenity,blank=True)
# Get the Amenity elements of a given Store
Store.objects.get(name='Downtown').amenities.all()
# Fetch store named Midtown
midtown_store = Store.objects.get(name='Midtown')
# Create and add Amenity element to Store
midtown_store.amenities.create(name='Laptop Lock',description='Ask our baristas...')
# Get all Store elements that have amenity id=3
Store.objects.filter(amenities__id=3)
Listing 8-28.Many to many ManyToManyField direct query read operations
在清单 8-28 中,您可以看到Store模型声明了与Amenity模型的ManyToManyField关系。一旦一个Store模型以这种方式与一个Amenity模型相关联,就可以使用 Python 的点符号访问Amenity模型,如清单 8-28 所示(例如,amenities.all()获取Store引用上所有相关的Amenity实例)。此外,清单 8-28 还展示了如何使用create()方法直接在模型amenities引用上创建Amenity实例。还要注意在清单 8-28 中,如何使用__(两个下划线)(也称为“跟随符号”)来创建引用相关模型的查询,以指示相关模型中的字段。
清单 8-28 中的操作使用与非关系模型相同的查询语法,因为这些操作是与具有关系数据类型的模型分开创建的。然而,Django 也支持在没有关系数据类型的模型上启动的 CRUD 操作。
清单 8-29 展示了通过Amenity模型的实例对其相关的Store模型进行的一系列 CRUD 操作。这些任务被称为反向操作,因为保持关系的模型——ManyToManyField——是反向实现的。
from coffeehouse.stores.models import Store, Amenity
wifi_amenity = Amenity.objects.get(name='WiFi')
# Fetch all Store records with Wifi Amenity
wifi_amenity.store_set.all()
# Get the total Store count for the Wifi Amenity
wifi_amenity.store_set.count()
# Fetch Store records that match a filter with the Wifi Amenity
wifi_amenity.store_set.filter(city__startswith='San Diego')
# Create a Store directly with the Wifi Amenity
# NOTE: Django also supports the get_or_create() and update_or_create() operations
wifi_amenity.store_set.create(name='Uptown',address='1240 University Ave...')
# Create a Store separately and then add the Wifi Amenity to it
new_store = Store(name='Midtown',address='844 W Washington St...')
new_store.save()
wifi_amenity.store_set.add(new_store)
# Create copy of breakfast items for later
wifi_stores = [ws for ws in wifi_amenity.store_set.all()]
# Clear all the Wifi amenity records in the junction table for all Store elements
wifi_amenity.store_set.clear()
# Verify Wifi count is now 0
wifi_amenity.store_set.count()
0
# Reassign Wifi set from copy of Store elements
wifi_amenity.store_set.set(wifi_stores)
# Verify Item count is now back to original count
wifi_amenity.store_set.count()
6
# Reassign Store set from copy of wifi stores
wifi_amenity.store_set.set(wifi_stores)
# Clear the Wifi amenity record from the junction table for a certain Store element
store_to_remove_amenity = Store.objects.get(name__startswith='844 W Washington St')
wifi_amenity.store_set.remove(store_to_remove_amenity)
# Delete the Wifi amenity element along with its associated junction table records for Store elements
wifi_amenity.delete()
Listing 8-29.Many to many ManyToManyField reverse query create, read, update, and delete operations with _set syntax
在清单 8-29 中,您可以看到多对多 Django 模型反向查询操作的各种例子。请注意与清单 8-26 和 8-27 中所示的一对多关系 CRUD 操作示例的相似之处。一对多和多对多关系中调用关系方法的显著区别如下:
- 当应用于多对多模型时,
add()和remove()关系方法分别使用模型的标准bulk_create()和delete()方法来添加和移除关系。这反过来意味着模型的标准save()方法没有被调用;因此,如果任一模型的save()方法执行定制逻辑,它将永远不会与add()和remove()关系方法一起运行。如果您想在创建或删除多对多关系时执行自定义逻辑,请使用m2m_changed信号。更多详情,请参见上一章“模型信号”一节。 - 如果为多对多模型关系声明一个自定义连接表,则
add()、create()、remove()和set()关系方法将被禁用。有关如何使用自定义连接表,请参阅上一章“数据库选项”小节中的“关系模型数据类型的选项”一节。
一对一 CRUD 操作
Django 一对一关系上的 CRUD 操作比前面的关系 CRUD 操作简单得多,因为一对一关系本质上要简单得多。在前一章中,您学习了一对一关系如何类似于继承层次结构,其中一个模型声明通用字段,另一个(相关的)模型继承前者的字段并添加更多的专用字段。
这意味着一对一的关系只有直接的查询操作,因为逆向操作没有意义,因为模型遵循层次结构。清单 8-30 显示了两个模型之间的一对多关系,包括对相关模型的一系列查询操作。
from coffeehouse.items.models import Item
# See Listing 8-25 for Item model definition
class Drink(models.Model):
item = models.OneToOneField(Item,on_delete=models.CASCADE,primary_key=True)
caffeine = models.IntegerField()
# Get Item instance named Mocha
mocha_item = Item.objects.get(name='Mocha')
# Access the Drink element and its fields through its base Item element
mocha_item.drink.caffeine
# Get Drink objects through Item with caffeine field less than 200
Item.objects.filter(drink__caffeine__lt=200)
# Delete the Item element and its associated Drink record
# NOTE: This deletes the associated Drink record due to the on_delete=models.CASCADE in the OneToOneField definition
mocha_item.delete()
# Query a Drink through an Item property
Drink.objects.get(item__name='Latte')
Listing 8-30.One to one OneToOneField query
operations
正如您在清单 8-30 中看到的,一对一 Django 模型关系的操作比前一个例子简单得多,尽管查询操作仍然使用相同的点符号在关系模型和字段中移动,以及使用__(两个下划线)(也称为“跟随符号”)在相关模型上按字段执行查询。
读取性能关系方法:select_related()和 prefetch_related()
在这些最后的 Django 模型关系 CRUD 操作部分——清单 8-25 到 8-30——您了解了通过关系字段从一个模型到另一个模型访问其他字段是多么容易。例如,对于一个Item和Menu模型之间的一对多关系,您可以使用语法item.menu.name访问一个Menu记录上的 name 字段;类似地,对于一个Store和Amenity模型之间的多对多关系,您可以使用语法store.amenities.all()[0].name访问一个Amenity记录上的name字段。
虽然这种点符号提供了一种轻松访问相关模型中的字段的方法——类似于defer()和load()方法轻松访问延迟数据的方式——但这种技术也会产生额外的数据库命中,这可以通过select_related()和prefetch_related()方法来防止。
selected_related()方法接受应该作为初始查询的一部分读取的相关模型字段参数。尽管这创建了一个更复杂的初始查询,但它避免了对相关模型字段的额外数据库命中。清单 8-31 展示了一个select_related()方法的例子,以及一个放弃使用它的查询。
from coffeehouse.items.models import Item
# See Listing 8-25 for Item and Menu model definitions
# Inefficient access to related model
for item in Item.objects.all():
item.menu # Each call to menu creates an additional database hit
# Efficient access to related model with selected_related()
for item in Item.objects.select_related('menu').all():
item.menu # All menu data references have been fetched on initial query
# Raw SQL query with select_related
print(Item.objects.select_related('menu').all().query)
SELECT "items_item"."id", "items_item"."menu_id", "items_item"."name", "items_item"."description", "items_item"."size", "items_item"."calories", "items_item"."price", "items_item"."stock", "items_menu"."id", "items_menu"."name" FROM "items_item" LEFT OUTER JOIN "items_menu" ON ("items_item"."menu_id" = "items_menu"."id")
# Raw SQL query without select_related
print(Item.objects.all().query)
SELECT "items_item"."id", "items_item"."menu_id", "items_item"."name", "items_item"."description", "items_item"."size", "items_item"."calories", "items_item"."price", "items_item"."stock" FROM "items_item"
Listing 8-31.Django model select_related syntax and generated SQL
在清单 8-31 中,您可以看到有两种方法可以访问所有Item模型记录的相关Menu模型。第一个变体使用Item.objects.all()语法来获得所有的Item模型记录,然后直接访问menu字段来获得对相应的Menu记录的访问。这种方法的问题是,获取每个Item记录的Menu记录会产生额外的数据库命中,所以如果您有 100 个Item记录,这意味着额外的 100 个数据库命中!
清单 8-31 中的第二个变化将select_related('menu')方法添加到查询中,确保每个Item记录的相关Menu记录也作为初始查询的一部分被获取。这种技术保证所有关系数据都在一个查询中提取。
在清单 8-31 的下半部分,您可以看到使用和省略select_related()方法时生成的原始 SQL。当使用select_related()时,使用更复杂的LEFT OUTER JOIN查询来确保在一个步骤中读取所有相关数据。
prefetch_related()方法解决了与select_related()方法相同的问题,但是使用了不同的技术。正如您在清单 8-31 中看到的,select_related()方法通过数据库连接的方式在单个查询中获取相关的模型数据;然而,一旦数据在 Python 中,prefetch_related()方法就执行它的连接逻辑。
虽然数据库连接解决了单个查询中的多查询问题,但它是一个重量级操作,通常很少使用。因此,select_related()方法仅限于单值关系(即ForeignKey和OneToOneField模型字段),因为与连接表相关的多值关系(即ManyToManyField)会在单个查询中产生大量数据。
当查询使用prefetch_related()方法时,Django 首先执行主查询,然后为在prefetch_related()方法中声明的所有相关模型生成QuerySet实例。所有这些都发生在一个步骤中,所以当您试图访问相关的模型引用时,Django 已经有了一个预先填充的相关结果的缓存,它以 Python 数据结构的形式连接这些结果以产生最终的结果。清单 8-32 展示了prefetch_related()方法的一个例子。
from coffeehouse.items.models import Item
from coffeehouse.stores.models import Store
# See Listing 8-25 for Item model definitions
# See Listing 8-28 for Store model definitions
# Efficient access to related model with prefetch_related()
for item in Item.objects.prefetch_related('menu').all():
item.menu # All menu data references have been fetched on initial query
# Efficient access to many to many related model with prefetch_related()
# NOTE Store.objects.select_related('amenities').all() is invalid due to many to many model
for store in Store.objects.prefetch_related('amenities').all():
store.amenities.all()
# Raw SQL query with prefetch_related
print(Item.objects.prefetch_related('menu').all().query)
SELECT "items_item"."id", "items_item"."menu_id", "items_item"."name", "items_item"."description", "items_item"."size", "items_item"."calories", "items_item"."price", "items_item"."stock" FROM "items_item"
# Raw SQL query with prefetch_related
print(Store.objects.prefetch_related('amenities').all().query)
SELECT "stores_store"."id", "stores_store"."name", "stores_store"."address", "stores_store"."city", "stores_store"."state", "stores_store"."email" FROM "stores_store"
Listing 8-32.Django model prefetch_related syntax and generated SQL
清单 8-32 中的第一个查询相当于清单 8-31 中的查询,它为所有Item模型记录获取相关的Menu模型,除了它使用了prefetch_related()方法。清单 8-32 中的第二个查询是在多对多模型关系上进行的,使用prefetch_related()方法获取所有Store模型记录的相关amenities模型实例。值得一提的是,最后一个查询只可能使用prefetch_related()方法,因为它是多对多模型关系。
最后,在清单 8-32 的下半部分,您可以确认使用prefetch_related()方法的查询生成的原始 SQL 显示为普通 SQL 查询(即,无连接)。在这种情况下,Django/Python 本身负责管理和创建高效读取相关模型数据所需的额外的QuerySet数据结构。
Tip
可以使用 prefetch()对象进一步优化 prefetch_related()方法,以进一步过滤预取操作或包含使用 selected_related。 1
通过 SQL 关键字对查询进行建模
在前面的章节中,您学习了如何使用 Django 模型方法查询单个、多个和相关的记录。然而,匹配过程大部分是在精确值的基础上完成的。例如,查询将id=1转换为 SQL WHERE ID=1的Store记录,或者查询将state="CA"转换为 SQL WHERE STATE="CA"的所有Store记录。
实际上,精确的 SQL 匹配模式与大多数需要更细粒度 SQL 查询的真实场景相差甚远。在接下来的小节中,您将了解按 SQL 关键字分类的各种 Django 模型查询选项,通过这种方式,您可以使用更广为人知的 SQL 关键字作为标识符,轻松识别所需的 Django 语法。
WHERE 查询:Django 字段查找
SQL WHERE 关键字是关系数据库查询中最常用的关键字之一,因为它用于通过字段值来限定查询中的记录数量。到目前为止,您主要使用 SQL WHERE 关键字来创建对精确值的查询(例如,WHERE ID=1);然而,SQL WHERE 关键字还有许多其他变体。
在 Django 模型中,SQL WHERE 关键字的变体通过字段查找得到支持,字段查找是使用__(两个下划线)(也称为“跟随符号”)附加到字段过滤器的关键字。
The PK Lookup Shortcut
Django 查询依靠模型字段名称来分类查询。例如,Django 查询中的 SQL WHERE ID=1 语句写成…(id=1),Django 查询中的 SQL WHERE NAME="CA "语句写成…(state="CA ")。
此外,Django 模型还可以使用 pk 快捷方式——其中 PK =“primary key”——对模型的主键执行查询。默认情况下,Django 模型的 id 字段是主键,因此 id 字段和 pk 快捷方式查询被认为是等效的(例如 store . objects . get(id = 1)store . objects . get(PK = 1))。
当模型定义了自定义主键模型字段时,具有 pk 查找的查询与具有 id 字段的查询只有不同的含义。
=/等于和!=/不相等查询:exact,iexact
等式或=查询是 Django 模型中使用的默认 WHERE 行为。等式搜索有两种语法变体;一个是简写版本,另一个使用exact字段查找,清单 8-33 展示了这两种方法。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
# Get the Store object with id=1
Store.objects.get(id__exact=1)
# Get the Store object with id=1 (Short-handed version)
Store.objects.get(id=1)
# Get the Drink objects with name="Mocha"
Item.objects.filter(name__exact="Mocha")
# Get the Drink objects with name="Mocha" (Short-handed version)
Item.objects.filter(name="Mocha")
Listing 8-33.Django equality = or EQUAL query
正如您在清单 8-33 中看到的,您可以使用exact字段查找来明确限定查询,或者使用简写语法<field>=<value>。因为 exact WHERE 查询是最常见的,Django 意味着默认的exact搜索。
Tip
您可以使用 iexact 字段查找进行不区分大小写的相等查询(例如,匹配“if”、“If”、“iF”或“IF”)。详情参见 LIKE 和 ILIKE 查询部分。
不平等还是!=搜索还有两种语法变体,如清单 8-34 所示。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
from django.db.models import Q
# Get the Store records that don't have state 'CA'
Store.objects.exclude(state='CA')
# Get the Store records that don't have state 'CA', using Q
Store.objects.filter(∼Q(state="CA"))
# Get the Item records and exclude items that have more than 100 calories
Item.objects.exclude(calories__gt=100)
# Get the Item records and exclude those with 100 or more calories, using Q
Item.objects.filter(∼Q(calories__gt=100))
Listing 8-34.Django inequality != or NOT EQUAL query with exclude() and Q objects
正如您在清单 8-34 中看到的,一种语法变体使用exclude()方法来排除匹配给定语句的对象。另一种方法是使用 Django Q对象来否定查询。在清单 8-34 中,您可以看到将状态值与CA匹配的Q对象Q(state="CA"),但是因为Q对象前面有∼(波浪符号),所以它是一个否定模式(即,匹配非CA的状态值)。
exclude()和Q对象语法产生相同的结果。Q对象主要用于更复杂的查询,但是在这种情况下,一个被否定的Q对象就像exclude()一样工作。
和查询
要使用 AND 语句创建 SQL WHERE 查询,您可以向一个查询添加多个语句或使用Q对象,如清单 8-35 所示。
from coffeehouse.stores.models import Store
from django.db.models import Q
# Get the Store records that have state 'CA' AND city 'San Diego'
Store.objects.filter(state='CA', city='San Diego')
# Get the Store records that have state 'CA' AND city not 'San Diego'
Store.objects.filter(Q(state='CA') & ∼Q(city='San Diego'))
Listing 8-35.Django AND query
清单 8-35 中的第一个例子将多个字段值添加到filter()方法中,以产生一个WHERE <field_1> AND <field_2>语句。清单 8-35 中的第二个例子也使用了filter()方法,但是使用了两个Q对象通过&操作符与AND语句(即WHERE <field_1> AND NOT <field2>)产生一个否定。
Tip
例如,如果您正在寻找一个比清单 8-35 中的查询更广泛的 AND 查询,获取状态为‘CA’的商店对象和状态为‘AZ’的商店对象,查看 or 查询或 in 查询。
如果您希望合并两个查询,例如 query1 和 query 2,请参阅本章后面的合并查询部分。
OR 查询:Q()对象
要用 OR 语句创建 SQL WHERE 查询,您可以使用Q对象,如清单 8-36 所示。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
from django.db.models import Q
# Get the Store records that have state 'CA' OR state='AZ'
Store.objects.filter(Q(state='CA') | Q(state='AZ'))
# Get the Item records with name "Mocha" or "Latte"
Item.objects.filter(Q(name="Mocha") | Q(name='Latte'))
Listing 8-36.Django OR query
清单 8-36 中的两个例子都在Q对象之间使用|(管道)操作符来产生一个WHERE <field1> OR <field2>语句,类似于&操作符如何用于 AND 条件。
IS 和 IS NOT 查询:isnull
SQL IS 和 IS NOT 语句通常在涉及空值的查询中与 WHERE 一起使用。根据数据库品牌的不同,SQL IS 和 IS NOT 也可以用于布尔查询。要创建带有 IS 或 NOT 语句的 SQL WHERE 查询,您可以使用带有等价测试的 Python None数据类型或isnull字段查找,如清单 8-37 所示。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Drink
from django.db.models import Q
# Get the Store records that have email NULL
Store.objects.filter(email=None)
# Get the Store records that have email NULL
Store.objects.filter(email__isnull=True)
# Get the Store records that have email NOT NULL
Store.objects.filter(email__isnull=False)
Listing 8-37.Django IS and IS NOT queries
清单 8-37 中的第一个例子试图查询 Python 的None值;在这种情况下,None被转换成 SQL 的 NULL(即IS NULL)。清单 8-37 中的第二个和第三个例子使用isnull字段查找,分别创建IS NULL和IS NOT NULL查询。
在查询中:在
SQL IN 语句与 WHERE 子句一起使用,生成与值列表匹配的查询。要用 IN 语句创建 SQL WHERE 查询,可以使用in字段查找,如清单 8-38 所示。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Drink
# Get the Store records that have state 'CA' OR state='AZ'
Store.objects.filter(state__in=['CA','AZ'])
# Get the Item records with id 1,2 or 3
Item.objects.filter(id__in=[1,2,3])
Listing 8-38.Django IN queries
正如您在清单 8-38 中所看到的,Django in字段查找可以用来创建一个查询,查找与来自任何字段(例如,整数、字符串)的列表值相匹配的记录。
LIKE 和 ILIKE 查询:contains、icontains、startswith、istartswith、endswith、iendswith
SQL LIKE 和 ILIKE 查询与 WHERE 子句一起使用来匹配字符串模式,前者区分大小写,后者不区分大小写。Django 提供了三种字段查找来生成类似 SQL 的查询,这取决于您希望匹配的字符串模式。清单 8-39 展示了如何使用 Django 字段查找生成三个不同的类似 SQL 的查询。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item, Drink
# Get the Store records that contain a 'C' anywhere in state (LIKE '%C%')
Store.objects.filter(state__contains='C')
# Get the Store records that start with 'San' in city (LIKE 'San%')
Store.objects.filter(city__startswith='San')
# Get the Item records that end with 'e' in name (LIKE '%e')
Drink.objects.filter(item__name__endswith='e')
Listing 8-39.Django LIKE queries
正如您在清单 8-39 中看到的,%符号代表一个 SQL 通配符,并根据 Django 字段查找放在 SQL LIKE 模式值的不同位置:要使用LIKE '%PATTERN%'生成一个 SQL 查询,您可以使用contains字段查找;为了生成带有LIKE 'PATTERN%'的 SQL 查询,您使用了startswith字段查找;为了生成带有LIKE '%PATTERN'的 SQL 查询,您可以使用endswith字段查找。
Django 还支持 SQL ILIKE 查询,它的功能类似于 LIKE 查询,但是不区分大小写。清单 8-40 展示了如何使用 Django 字段查找创建 ILIKE 查询。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
# Get the Store recoeds that contain 'a' in state anywhere case insensitive (ILIKE '%a%')
Store.objects.filter(state__icontains='a')
# Get the Store records that start with 'san' in city case insensitive (ILIKE 'san%')
Store.objects.filter(city__istartswith='san')
# Get the Item records that end with 'a' in name case insensitive (ILIKE '%A')
Item.objects.filter(name__iendswith='A')
# Get the Store records that have state 'ca' case insensitive (ILIKE 'ca')
Store.objects.filter(state__iexact='ca')
Listing 8-40.Django ILIKE queries
清单 8-40 中的例子就像清单 8-39 中的例子一样,但是唯一的区别是 Django 的字段查找以字母 I 开头,表示不区分大小写的 ILIKE 查询。
值得一提的是清单 8-40 中的最后一个例子是=/EQUAL and 的不区分大小写版本!=/不等于查询。然而,因为iexact在引擎盖下使用了 ILIKE,所以在本节中再次提到它。
REGEXP 查询:regex,iregex
有时 SQL LIKE & ILIKE 语句支持的模式过于基本,在这种情况下,您可以使用 SQL REGEXP 语句将复杂模式定义为正则表达式。正则表达式更强大,因为它们可以定义碎片化的模式,例如:以 sa 开头,后面跟任意字母,后面跟一个数字的模式;或者条件模式,例如以 Los 开始或以 Angeles 结束模式。Django 通过regex字段查找支持 SQL REGEXP 关键字,还通过iregex字段查找支持不区分大小写的正则表达式查询。
尽管描述许多正则表达式语法变体超出了我们的讨论范围,但是一个匹配带有以Los或San开头的city的Store记录的示例正则表达式查询应该是:Store.objects.filter(city__regex=r'^(Los|San) +')。
注定义regex或iregex字段查找模式的推荐实践是使用 Python 原始字符串。Python 原始字符串文字是一个以r开头的字符串,它方便地表达将被转义序列处理修改的字符串(例如,原始字符串r'\n'与标准字符串'\\n'相同)。这种行为对于严重依赖转义字符的正则表达式尤其有用。附录 A 更详细地描述了 Python 原始字符串的使用。
>/大于和小于查询:gt,gte,lt,lte
与数字字段相关联的 SQL WHERE 语句通常使用数学运算符>、> =、> =、< and <= through the 【 , 【 , 【 , and 【 field lookups, respectively. Listing 8-41 说明了这些字段查找在 Django 中的用法。
from coffeehouse.items.models import Item
# Get Item records with stock > 5
Item.objects.filter(stock__gt=5)
# Get Item records with stock > or equal 10
Item.objects.filter(stock__gte=10)
# Get Item records with stock < 100
Item.objects.filter(stock__lt=100)
# Get Item records with stock < or equal 50
Item.objects.filter(stock__lte=50)
Listing 8-41.Django GREATER THAN and LESSER THAN queries
日期和时间查询:范围,日期,年,月,日,周,星期 _ 日,时间,小时,分钟,秒
虽然 SQL WHERE 对日期和时间字段的查询可以用等号、大于号和小于号来完成,但由于它们的特殊性质,编写 SQL 日期和时间查询可能会很耗时。例如,要创建一个 SQL 查询来获取时间戳为 2018 年的所有记录,您需要创建一个类似于'WHERE date BETWEEN '2018-01-01' AND '2018-12-31'的查询。如您所见,如果您添加了处理时区、月份和闰年等内容的需求,这些查询在语法上可能会变得复杂和容易出错。
为了简化带有日期和时间值的 SQL WHERE 查询的创建,Django 提供了各种字段查找,如清单 8-42 所示。
from coffeehouse.online.models import Order
from django.utils.timezone import utc
import datetime
# Define custom dates
start_date = datetime.datetime(2017, 5, 10).replace(tzinfo=utc)
end_date = datetime.datetime(2018, 5, 21).replace(tzinfo=utc)
# Get Order recrods from custom dates, starting May 10 2017 to May 21 2018
Order.objects.filter(created__range=(start_date, end_date))
# Get Order records with exact start date
orders_2018 = Order.objects.filter(created__date=start_date)
# Get Order records with year 2018
Order.objects.filter(created__year=2018)
# Get Order records with month January, values can be 1 through 12 (1=January, 12=December).
Order.objects.filter(created__month=1)
# Get Order records with day 1, where values can be 1 through 31.
Order.objects.filter(created__day=1)
# Get Order records from January 1 2018
Order.objects.filter(created__year=2018,create__month=1,created__day=1)
# Get Order records that fall on week number 24 of the yr, where values can be 1 to 53.
Order.objects.filter(created__week=24)
# Get Order recrods that fall on Monday, where values can be 1 to 7 (1=Sunday, 7=Saturday).
Order.objects.filter(created__week_day=2)
# Get Order records made at 2:30pm using a time object
Order.objects.filter(created__time=datetime.time(14, 30))
# Get Order records made at 10am, where values can be 0 to 23 (0=12am, 23=11pm).
Order.objects.filter(date__hour=10)
# Get Order records made at the top of the hour, where values are 0 to 59.
Order.objects.filter(date__minute=0)
# Get Order records made the 30 second mark of every minute, where values are 0 to 59.
Order.objects.filter(date__second=30)
Listing 8-42.Django date and time queries with field lookups
清单 8-42 中的第一个例子使用了range字段查找,它使用两个 Python datetime.datetime对象来定义查询的日期范围。虽然range是创建日期和时间查询最灵活的方法,但是还有其他提供更简单语法的字段查找替代方法。date字段查找允许您创建一个精确日期的查询。
year、month和day字段查找允许您创建分别匹配给定年、月或日的记录的查询。此外,如果您查看清单 8-42 的中间部分,您会注意到还可以创建一个带有多个字段查找的查询来匹配年、月和日的组合。
最后,在清单 8-42 的下半部分,您可以看到week和week_day字段查找可以分别为匹配一年中给定的一周或一周中的某一天的记录创建一个查询。除了设计用于基于datetime.time对象进行查询的time字段查找,以及设计用于分别为匹配给定小时、分钟或秒的记录创建查询的hour、minute和second字段查找。
Tip
要进行只从记录(而不是完整记录)中提取日期和时间的查询,请查看日期和时间子部分下的 DISTINCT 部分。
Can’T Find an SQL Where Statement? Custom Lookups, Extra(), Subqueries, or Raw Query
尽管 Django 提供了一个广泛的字段查找列表来生成各种 SQL WHERE 语句,但这并不意味着您总能找到必要的字段查找来生成所需的 SQL WHERE 语句。在这种情况下,您有以下选择:
- 创建自定义查找:就像其他 Django 自定义构造一样,您可以使用自定义 SQL WHERE 语句创建自定义查找。 2
- 使用 extra()方法:Django model extra()方法也可以用来创建定制的 SQL WHERE 语句。 3
- 使用子查询:子查询允许创建依赖于其他查询结果的 WHERE 语句。本章后面的部分将介绍如何在 Django 模型上创建 SQL 子查询。
- 原始 SQL 查询:您可以用一字不差的 SQL WHERE 语句创建一个原始(开放式)SQL 查询。本章后面的部分将介绍如何在 Django 模型上执行原始 SQL 查询。
独特的查询
SQL DISTINCT 关键字用于过滤重复记录,并通过distinct()方法在 Django 模型中得到支持。默认情况下,SQL DISTINCT 和 Django distinct()方法应用于整个记录的内容。这意味着除非一个查询限制它的字段数量或者一个查询跨越多个模型,否则distinct()方法将永远不会产生不同的结果。清单 8-43 展示了几个使用distinct()方法的查询,它们更好地展示了这种行为。
from coffeehouse.stores.models import Store
# Get all Store records number
Store.objects.all().count()
4
# Get all distinct Store record number
Store.objects.distinct().count()
4
# Get distinct state Store record values
Store.objects.values('state').distinct().count()
1
# ONLY for PostgreSQL, distinct() can accept model fields to create DISTINCT ON query
Store.objects.distinct('state')
Listing 8-43.Django DISTINCT queries with distinct()
清单 8-43 中的第一个查询获得所有Store记录的总数,而第二个查询获得不同Store记录的总数。请注意,尽管第二个查询使用了distinct()方法,但是两个计数是相同的,因为所有记录中至少有一个字段值(例如id)是不同的。
清单 8-43 中的第三个查询利用values()方法将查询记录限制在仅state字段。一旦完成,将对查询应用distinct()方法,然后应用count()方法,以获得不同的state值的总数。通过在 distinct()方法之前应用选择性查询字段方法(例如,values()或values_list()),distinct()方法执行的逻辑产生逻辑输出。
清单 8-43 中的最后一个例子将一个模型字段传递给distinct()方法,以在查询时产生一个 SQL DISTINCT。只有理解 SQL DISTINCT ON 语句的 PostgreSQL 数据库才支持最后一个distinct()方法语法。
日期和时间查询:日期()和日期时间()
除了distinct()方法,Django 还提供了两种特殊的方法,用于从记录中提取不同的日期和时间值。dates()和datetimes()方法基于匹配不同日期或时间的模型记录值生成一个datetime.date或datetime.datetime对象的列表。
dates()方法接受三个参数,两个是必需的,一个是可选的。第一个参数(必选)是执行不同查询的日期字段,第二个参数(必选)是执行不同查询的日期部分,可以是'year'、'month'或'day'。第三个参数(可选)是查询顺序,默认为'ASC'升序,但也可以是'DESC'降序。
datetimes()方法也接受三个参数,两个是必需的,一个是可选的。第一个参数(必选)是执行不同查询的日期时间字段,第二个参数(必选)是执行不同查询的日期时间部分,可以是'year'、'month'、'day'、'hour'、'minute'或'second'。第三个参数(可选)是查询顺序,默认为'ASC'升序,但也可以是'DESC'降序。
清单 8-44 展示了一系列使用dates()和datetimes()方法的例子。
from coffeehouse.online.models import Order
# Get distinct years (as datetime.date) for Order objects
Order.objects.dates('created','year')
# Outputs: <QuerySet [datetime.date(2017, 1, 1),datetime.date(2018, 1, 1)]>
# Get distinct months (as datetime.date) for Order objects
Order.objects.dates('created','month')
# Outputs: <QuerySet [datetime.date(2017, 3, 1),datetime.date(2017, 6, 1),datetime.date(2018, 2, 1)]>
# Get distinct days (as datetime.datetime) for Order objects
Order.objects.datetimes('created','day')
# Outputs: <QuerySet [datetime.datetime(2017, 6, 17, 0, 0, tzinfo=<UTC>)...]>
# Get distinct minutes (as datetime.datetime) for Order objects
Order.objects.datetimes('created','minute')
# Outputs: <QuerySet [datetime.datetime(2017, 6, 17, 3, 13, tzinfo=<UTC>)...]>
Listing 8-44.Django DISTINCT date and time queries with dates and datetimes() methods
正如您在清单 8-44 中所看到的,dates()方法产生一个由所有模型记录中的给定日期组件生成的datetime.date对象的列表,而datetimes()方法产生一个由所有模型记录中的给定日期时间组件生成的datetime.datetime对象的列表。注意清单 8-44 中的例子将dates()和datetimes()方法应用于所有的模型记录,但是在任何查询(即filter()或exclude())上使用这些方法都是有效的。
Tip
您还可以使用聚合查询来计算不同的值。有关此过程的更多详细信息,请参见聚合查询部分。
订单查询:order_by()和 reverse()
SQL 查询通常使用 ORDER 关键字告诉数据库引擎根据某个或某些字段对查询结果进行排序。这种技术很有帮助,因为它避免了在数据库之外(即在 Python 中)对记录进行排序的额外开销。Django 模型通过order_by()方法支持 SQL ORDER 语句。order_by()方法接受模型字段作为输入来定义查询顺序,这个过程如清单 8-45 所示。
from coffeehouse.stores.models import Store
# Get Store records and order by city (ORDER BY city)
Store.objects.all().order_by('city')
# Get Store recrods, order by name descending, email ascending (ORDER BY name DESC, email ASC)
Store.objects.filter(city='San Diego').order_by('-name','email')
Listing 8-45.Django ORDER queries
清单 8-45 中的第一个例子为所有按照城市排序的Store对象定义了一个查询。默认情况下,order_by设置升序(即“A”记录在前,“Z”记录在后)。清单 8-45 中的第二个例子定义了一个Store查询,但是有多个字段,所以查询首先按第一个字段排序,然后按第二个排序。此外,第二个示例说明了如何使用–(减号)来覆盖默认的升序,-name表示按name对记录进行排序,但以降序排序(即,首先是“Z”记录,最后是“A”记录)。
Tip
您可以在模型上声明 ordering Meta 选项来设置其默认查询排序行为,而不是声明 order_by()方法。参见前一章的查询元选项部分。
除了order_by()方法,Django 模型还支持反转QuerySet结果的reverse()方法。reverse()方法的工作方式就像 Python 的标准reverse()方法一样,颠倒列表的顺序,除了它被设计成在数据被具体化之前对 Django QuerySet数据结构进行操作。
限制查询
当您希望避免在查询中读取整个记录集,而是将结果记录限制在一个较小的记录集中时,可以使用 SQL LIMIT 语句。SQL LIMIT 语句有助于您有目的地逐步读取查询记录(例如,显示在多个页面上的大型查询,也称为分页)-或者您想要对查询进行采样(例如,获取查询中的第一个、最后一个、最新或最早的记录)。
Django 模型提供了各种机制来生成极限查询,这将在下一节中描述。
限制和偏移查询:Python 切片语法
SQL 限制查询通常伴随着 OFFSET 语句,最后一个语句用于从整个记录集中的给定点开始提取记录。Django 模型支持使用标准 Python slice 语法(即用于分割列表的相同语法)创建带有 LIMIT 和 OFFSET 语句的 SQL 查询。清单 8-46 说明了如何生成极限和偏移查询。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
# Get the first five (LIMIT=5) Store records that have state 'CA'
Store.objects.filter(state='CA')[:5]
# Get the second five (OFFSET=5,LIMIT=5) Item records (after the first 5)
Item.objects.all()[5:10]
# Get the first (LIMIT=1) Item object
Item.objects.all()[0]
Listing 8-46.Django LIMIT and OFFSET queries with Python slice syntax
正如您在清单 8-46 中看到的,生成限制和偏移查询的技术是通过 Python 的切片语法直接应用于QuerySet数据结构。如果您从未使用过 Python 的 slice 语法,该技术非常简单:语法QuerySet[start:end]从QuerySet的start到end-1获取项目,语法QuerySet[start:]从start到QuerySet的其余部分获取项目,语法QuerySet[:end]从QuerySet的开头到end-1获取项目。
伪极限 1 阶查询:first()和 last()
在某些情况下,SQL LIMIT 语句用于获取单个记录,即一组记录中的第一条或最后一条记录。Django 模型支持 first()和 last()方法,这些方法生成一个 LIMIT 1 查询,就好像您用 slice 语法[0]创建了一个查询一样——如清单 8-46 中所述。
first()和last()方法通常在order_by()模型方法之前,以保证预期的记录顺序,从而获得所述记录的第一个或最后一个记录。如果在没有使用order_by()模型方法的情况下应用了first()和last()方法,那么查询将根据默认排序机制(通过id字段)来应用,因此first()返回具有第一个id值的记录,而last()返回具有最后一个id值的记录。
例如,查询Store.objects.filter(state='CA').first()获得第一个Store记录,其中state='CA'的id最低(因为订单默认为 id),这个查询相当于Store.objects.filter(state='CA')[0]。查询Item.objects.all().order_by('name').last()获取字母表中最后一个带有name的条目记录(因为顺序是由name指定的),这个查询相当于Item.objects.all().order_by('name').reverse()[0]。
伪限制 1 日期和时间查询:最晚()和最早()
对于与日期或时间相关联的 SQL 限制查询,Django 提供了latest()和earliest()方法来获得基于日期字段的最近或第一次创建的模型记录。与first()和last()相比,latest()和earliest()方法都接受执行查询的日期字段,并提供更短的语法来处理与日期或时间相关的有限查询。这是因为latest()和earliest()方法自动对作为参数提供的字段执行order_by()操作。
例如,Order.objects.latest('created')根据created字段获得最近的Order记录,而Order.objects.earliest('created')根据created字段获得最早的Order记录。
Tip
在模型中使用 get_latest_by 元选项设置一个默认字段,在该字段上执行 latest()和 earliest()方法。有关更多详细信息,请参见上一章的元选项。
合并查询
SQL 查询经常需要合并以产生不同的结果集,比如合并多个 SQL 查询的记录或者获取多个 SQL 查询之间的公共记录。Django 支持各种方式来合并 SQL 查询,既可以作为QuerySet数据结构,也可以通过 UNION、INTERSECT 和 EXCEPT 等 SQL 查询语句。
QuerySet 合并:Pipe 和 itertools.chain
正如您在本章中所了解到的,Django 模型通常使用QuerySet数据结构来表示 SQL 查询。这种QuerySet数据结构通常需要合并,以呈现更大的结果集,并避免执行新的数据库查询。清单 8-47 展示了两种可用于合并QuerySet数据结构的语法变体。
from coffeehouse.items.models import Item, Drink
from itertools import chain
menu_sandwich_items = Item.objects.filter(menu__name='Sandwiches')
menu_salads_items = Item.objects.filter(menu__name='Salads')
drinks = Drink.objects.all()
# A pipe applied to two QuerySets generates a larger QuerySet
lunch_items = menu_sandwich_items | menu_salads_items
# | can't be used to merge QuerySet's with different models # ERROR menu_sandwich_items | drinks
# itertools.chain generates a Python list and can merge different QuerySet model types
lunch_items_with_drinks = list(chain(menu_sandwich_items, drinks))
Listing 8-47.Combine two Django queries with | (pipe) and itertools.chain
清单 8-47 中的第一个选项使用|(管道)操作符来组合两个QuerySet数据结构。这种技术产生了另一种QuerySet数据结构,但是有一个警告,即只能在使用相同模型的QuerySet上工作(例如Item)。
清单 8-47 中的第二个选项使用 Python itertools包通过chain()方法合并两个QuerySet数据结构。这种技术产生一个标准的 Python 列表——带有各自的模型对象——并且是更灵活的选择,因为它可以组合QuerySet数据结构,即使它们使用不同的模型(例如Item和Drink)。
联合查询:UNION()
SQL UNION 语句用于在数据库中直接合并两个或多个查询。不像前面的合并查询技术——如清单 8-47 所示——发生在 Django/Python 中,联合查询完全由数据库引擎完成。Django 通过union()方法支持 SQL UNION 语句,如清单 8-48 所示。
from coffeehouse.items.models import Item
menu_breakfast_items = Item.objects.filter(menu__name='Breakfast')
menu_sandwich_items = Item.objects.filter(menu__name='Sandwiches')
menu_salads_items = Item.objects.filter(menu__name='Salads')
# All items merged with union()
all_items = menu_breakfast_items.union(menu_sandwich_items,menu_salads_items)
print(all_items.query)
SELECT "items_item"."id", "items_item"."menu_id" ... WHERE "items_menu"."name" = Breakfast UNION SELECT "items_item"."id", "items_item"."menu_id" ... WHERE "items_menu"."name" = Sandwiches UNION SELECT "items_item"."id", "items_item"."menu_id"... WHERE "items_menu"."name" = Salads
Listing 8-48.Merge Django queries with union()
清单 8-48 首先声明了三个产生QuerySet数据结构的标准 SQL 查询。接下来,注意union()方法是如何链接到一个查询的,其余的查询是作为参数传递的。最后,清单 8-48 展示了union()方法的结果如何产生一个包含多个 SQL UNION 语句的查询,这些语句合并了各个查询。
除了union()方法接受不同的QuerySet实例作为参数之外,union()方法还接受设置为False的可选关键字all参数。默认情况下,union()方法忽略跨QuerySet实例的重复值;但是,您可以将all参数设置为True来告诉 Django 合并重复的记录(例如menu_breakfast_items.union(menu_sandwich_items,menu_salads_items, all=True))。
相交查询:相交()
SQL INTERSECT 语句用于获取在多个查询中相交(即存在)的记录。Django 通过intersection()方法支持 SQL INTERSECT 语句,如清单 8-49 所示。
from coffeehouse.items.models import Item
all_items = Item.objects.all()
menu_breakfast_items = Item.objects.filter(menu__name='Breakfast')
# Intersected (common) records merged with intersect()
intersection_items = all_items.intersection(menu_breakfast_items)
print(intersection_items.query)
SELECT "items_item"."id", "items_item"."menu_id", "items_item"."name"... INTERSECT SELECT "items_item"."id", "items_item"."menu_id", "items_item"."name"... WHERE "items_menu"."name" = Breakfast
Listing 8-49.Intersect (Common) Django query records with intersection()
清单 8-49 首先声明了两个产生QuerySet数据结构的标准 SQL 查询。接下来,注意intersection()方法是如何链接到其中一个查询的,剩下的查询是如何作为参数传递的。最后,清单 8-49 展示了intersection()方法的结果如何产生一个带有 SQL 交集语句的查询,从而产生跨查询的公共记录。
intersection()方法只接受QuerySet实例作为参数。此外,在一个intersection()查询中声明两个以上的QuerySet实例时要小心,因为只有出现在所有QuerySet实例中的记录才构成最终查询结果的一部分。
例外查询:差异()
SQL EXCEPT 语句用于获取在查询中出现但在其他查询中缺失的记录。Django 通过difference()方法支持 SQL EXCEPT 语句,如清单 8-50 所示。
from coffeehouse.items.models import Item
all_items = Item.objects.all()
menu_breakfast_items = Item.objects.filter(menu__name='Breakfast')
menu_sandwich_items = Item.objects.filter(menu__name='Sandwiches')
menu_salads_items = Item.objects.filter(menu__name='Salads')
# Extract records in all_items, except those in:
# menu_breakfast_items, menu_sandwich_items & menu_salads_items
ex_items = all_items.difference(menu_breakfast_items, menu_sandwich_items, menu_salads_items)
print(ex_items.query)
SELECT "items_item"."id", "items_item"."menu_id", "items_item"."name"...EXCEPT SELECT "items_item"."id", "items_item"."menu_id", "items_item"."name"... EXCEPT SELECT "items_item"."id", "items_item"."menu_id", "items_item"."name", ... EXCEPT SELECT "items_item"."id", "items_item"."menu_id", "items_item"."name" ... WHERE "items_menu"."name" = Salads
Listing 8-50.Except Django query records with difference()
清单 8-50 首先声明了产生QuerySet数据结构的四个标准 SQL 查询。接下来,注意如何在all_items查询上调用difference()方法,以及如何将剩余的查询作为要从all_items查询中排除的参数进行传递。最后,清单 8-50 展示了difference()方法的结果如何产生一个包含多个 SQL EXCEPT 语句的查询,这些语句从父查询中排除查询记录。
聚合查询
SQL 查询有时需要从 Django 模型中包含的核心字段生成值(例如,记录集的计数、平均值、最大值或最小值等数学计算)。将这种类型的聚合信息存储为单独的 Django 模型字段是多余的——因为它可以从核心数据中导出——并且在数据库环境之外计算这些数据也是浪费的(例如,读取所有记录并在 Python 中生成聚合结果)。
SQL 通过聚合函数为数据库提供了必要的语句来解决这个问题。聚合函数是 SQL 查询的一部分,由数据库引擎执行,并在与aggregate()方法结合使用时作为独立的结果返回,或者在与annotate()方法结合使用时作为附加字段与结果 SQL 响应一起返回。Django 通过一系列方法支持聚合查询,包括count()、aggregate()和annotate(),以及聚合类。
计数查询:count()方法和 COUNT()类
SQL COUNT 聚合函数用于这样的情况:您只需要获取符合特定条件的记录数,而不需要读取构成查询的所有记录。使用 SQL COUNT 的查询也更有效,因为是数据库引擎进行计算,而不是获取所有数据并在 Python 中进行计算。
Django models 通过count()方法和 aggregate Count类支持 SQL 计数聚合函数。两种变化都在清单 8-51 中进行了说明。
from coffeehouse.stores.models import Store
from django.db.models import Count
# Get the number of stores (COUNT(*))
stores_count = Store.objects.all().count()
print(stores_count)
4
# Get the number of stores that have city 'San Diego' (COUNT(*))
stores_san_diego_count = Store.objects.filter(city='San Diego').count()
# Get the number of emails, NULL values are not counted (COUNT(email))
emails_count = Store.objects.aggregate(Count('email'))
print(emails_count)
{'email__count': 4}
# Get the number of emails, NULL values are not counted (COUNT(email) AS "coffeehouse_store_emails_count")
emails_count_custom = Store.objects.aggregate(coffeehouse_store_emails_count=Count('email'))
print(emails_count_custom)
{'coffeehouse_store_emails_count': 4}
# Get number of distinct Amenities in all Stores, NULL values not counted (COUNT(DISTINCT name))
different_amenities_count = Store.objects.aggregate(Count('amenities',distinct=True))
print(different_amenities_count)
{'amenities__count': 5}
# Get number of Amenities per Store with annotate
stores_with_amenities_count = Store.objects.annotate(Count('amenities'))
# Get amenities count in individual Store
stores_With_amenities_count[0].amenities__count
# Get number of Amenities per Store with annotate and custom name
stores_amenities_count_custom = Store.objects.annotate(amenities_per_store=Count('amenities'))
stores_amenities_count_custom[0].amenities_per_store
Listing 8-51.Django COUNT queries with aggregate(), annotate(), and Count()
清单 8-51 中的前两个例子将count()方法作为 Django 查询的最后一部分,以获得总计数。
清单 8-51 中的第三个例子使用aggregate()函数和聚合Count类来获得Store记录中emails的总数。请注意使用aggregate()方法的查询是如何生成字典的,其中的键是 counted 字段——在本例中是email——后缀是__count,表示聚合类,字典值是结果计数。清单 8-51 中的第四个例子与第三个非常相似,除了它在聚合Count类前添加了一个自定义字符串来模拟 SQL AS 关键字,因此得到的字典值使用自定义字符串coffeehouse_store_emails_count作为关键结果。
Note
如果在查询中没有将字符串分配给聚合类(例如 Count),则结果查询输出默认为: __ <aggregate_class>。</aggregate_class>
清单 8-51 中的第五个例子说明了 aggregate Count类如何接受可选的distinct=True参数来忽略计数中的重复值。在这种情况下,对所有与Store记录相关联的amenities进行计数,但是该计数仅反映不同的amenities值。
虽然aggregate()方法产生聚合结果,但是它仅限于自己产生聚合结果,也就是说,它需要额外的查询来获取计算聚合结果的核心数据。annotate()方法解决了这个问题,如清单 8-51 所示。
清单 8-51 中的最后两个例子使用annotate()方法向查询记录添加一个额外的字段来保存一个聚合结果。清单 8-51 中倒数第二个例子通过聚合Count()类将amenities__count字段添加到所有Store记录中。清单 8-51 中的最后一个例子为聚合Count()类分配了一个自定义字符串,以创建自定义amenities_per_store字段来保存所有Store记录的amenities计数。
Max、Min、Sum、AVG、方差和 StdDev 查询:MAX()、MIN()、SUM()、Avg()、VARIANCE()和 STDDEV()类
除了 SQL 计数聚合函数之外,SQL 查询还支持其他聚合函数,用于最好在数据库中完成的数学运算。这些 SQL 聚合函数包括从一组记录中获取最大值的 MAX、从一组记录中获取最小值的 MIN、从一组记录中获取值的 SUM、从一组记录中获取平均值的 AVG、从一组记录中获取值的统计方差的 VARIANCE 以及从一组记录中获取统计偏差的 STDDEV。
Django 模型通过使用聚合类支持所有以前的 SQL 聚合函数——就像清单 8-51 中描述的Count()聚合一样。因此,为了利用这些额外的 SQL 聚合函数,您可以将 Django 模型的aggregate()或annotate()方法与相关的聚合类结合使用,清单 8-52 展示了这个过程。
from coffeehouse.items.models import Item
from django.db.models import Avg, Max, Min
from django.db.models import Sum
from django.db.models import Variance, StdDev
# Get the average, maximum and minimum number of stock for all Item records
avg_max_min_stock = Item.objects.aggregate(Avg('stock'), Max('stock'), Min('stock'))
print(avg_max_min_stock)
{'stock__avg': 29.0, 'stock__max': 36, 'stock__min': 27}
# Get the total stock for all Items
item_all_stock = Item.objects.aggregate(all_stock=Sum('stock'))
print(item_all_stock)
{'all_stock': 261}
# Get the variance and standard deviation for all Item records
# NOTE: Variance & StdDev return the population variance & standard deviation, respectively.
# But it's also possible to return sample variance & standard deviation,
# using the sample=True argument
item_statistics = Item.objects.aggregate(Variance('stock'), std_dev_stock= StdDev('stock'))
{'std_dev_stock': 5.3748, 'stock__variance': 28.8888}
Listing 8-52.Django MAX, MIN,SUM, AVG, VARIANCE and STDDEV queries with Max(), Min(), Sum(), Avg(), Variance() and StdDev() classes
正如您在清单 8-52 中的第一个例子中所看到的,作为aggregate()方法的一部分,可以为一个查询定义多个聚合类,在这种情况下,查询获得所有Item记录的平均值、最小值和最大值stock。
清单 8-52 中的第二个例子通过使用 aggregate Sum类获得所有条目记录中所有stock值的总和。请注意,在第二个示例中,如何在 aggregate 类前面加上一个自定义字符串作为 SQL AS 关键字,以便查询输出与 aggregate 类名不同的结果值。最后,清单 8-52 中的最后一个例子计算所有项目记录中所有stock值的方差和标准差。
Tip
如果要执行更复杂的聚合查询,如多字段数学运算(如乘法),请参阅 F 表达式小节。
如果要执行更复杂的聚合查询,请参阅使用原始(开放式)SQL 的模型查询一节。
表达式和函数查询
SQL 查询通常引用由调用环境提供的值,而不考虑它们的许多语句。例如,当您创建一个查询来获取所有具有某些state值的Store记录时,Django/Python 为state提供一个值引用,同样,如果您创建一个查询来获取属于某个Menu模型的所有Item记录,Django/Python 为Store提供一个值引用。
但是,对于某些 SQL 查询,有必要使用指向实际数据库中数据的引用。这是必要的,因为某些 SQL 查询的结果取决于数据库中存在的数据,或者因为在数据库(即 Python)的上下文之外操作数据是一项额外的工作,可以很容易地在 SQL 中解决。
您已经在前面关于聚合查询的章节中了解了这种技术,其中 SQL 查询可以告诉数据库引擎计算诸如计数和平均值之类的东西,而不需要提取数据并在数据库之外执行操作(例如,用 Python)。聚合查询依赖于一个特殊的表达式子集,称为聚合表达式,但是在接下来的章节中,您将了解 Django 如何支持许多其他类型的 SQL 表达式。
另一种 SQL 技术是 SQL 函数,它以 SQL 表达式的相同精神设计,有利于将工作委托给数据库引擎。SQL 函数旨在允许数据库改变查询的结果(例如,连接两个字段或将一个字段转换为大写/小写),并减轻在呼叫方环境(即,Django/Python)中对这种任务的需要。在下一节中,您还将了解 Django 模型支持的不同 SQL 函数。
SQL 表达式查询:F 表达式
Django F 表达式是 Django 模型中最常见的 SQL 表达式。在本章开始时,您已经了解了 F 表达式的用途,了解了如何在一个步骤中更新一条记录,并让数据库引擎执行逻辑,而不需要从数据库中提取记录。
通过 F 表达式,可以在查询中引用模型字段,并让数据库对模型字段值执行操作,而无需从数据库中提取数据。反过来,这不仅提供了更简洁的查询语法——单个更新查询,而不是两个(一个读取,一个更新)——它还避免了“竞争条件”。 4
清单 8-53 展示了在更新查询中使用 F 表达式的各种方式。
from coffeehouse.items.models import Item
from django.db.models import F
# Get single item
egg_biscuit = Item.objects.get(id=2)
# Check stock
egg_biscuit.stock
2
# Add 10 to stock value with F() expression
egg_biscuit.stock = F('stock') + 10
# Trigger save() to apply F() expression
egg_biscuit.save()
# Check stock again
egg_biscuit.stock
<CombinedExpression: F(stock) + Value(10)>
# Ups, need to re-read/refresh from DB
egg_biscuit.refresh_from_db()
# Check stock again
egg_biscuit.stock
12
# Decrease stock value by 1 for Item records on the Breakfast menu
breakfast_items = Item.objects.filter(menu__name='Breakfast')
breakfast_items.update(stock=F('stock') – 1)
# Increase all Item records stock by 20
Item.objects.all().update(stock=F('stock') + 20)
Listing 8-53.Django F() expression
update queries
清单 8-53 中的第一个例子读取单个模型记录,并将一个F()表达式应用于stock字段。一旦应用了F()表达式,就需要对数据库的记录调用save()方法来触发更新。接下来,请注意,为了让模型记录引用反映出F()表达式的结果,您必须从数据库中重新读取记录——在本例中使用的是refresh_from_db()方法——因为数据库是唯一知道更新操作结果的一方。
接下来在清单 8-53 中,您可以看到如何对一个F()表达式执行减法运算,以及通过update()方法将一个F()表达式应用到一个QuerySet中的所有记录。
除了更新记录而不需要从数据库中提取数据之外,F()表达式还可以用于数据库读取操作。F()表达式对于读取查询和聚合查询很有帮助,这些查询的结果最好由数据库引擎决定,如清单 8-54 所示。
from django.db.models import F, ExpressionWrapper, FloatField
from coffeehouse.items.models import Drink, Item
calories_dbl_caffeine_drinks = Drink.objects.filter(item__calories__gt=F('caffeine')*2)
items_with_assets = Item.objects.annotate(
assets=ExpressionWrapper(F('stock')*F('price'),
output_field=FloatField()))
Listing 8-54.Django F() expressions in read queries and aggregate queries
请注意清单 8-54 中的第一个查询示例缺少任何固定值,而是由数据库引擎检查的引用组成,以返回符合条件的记录。在这种情况下,查询获得所有calories大于其caffeine内容两倍的Drink记录,其中其数据库引擎——通过F()表达式——负责确定哪些Drink记录符合该规则。
清单 8-54 中的第二个例子从两个F()表达式中创建一个聚合查询。在这种情况下,通过F()表达式将记录的stock和price字段的值相乘,用annotate()方法计算出一个名为assets的新字段。与上一节中的聚合查询示例不同,此聚合有两个重要的差异和参数:
ExpressionWrapper。-因为清单 8-54 中的聚集查询由多个模型字段组成,所以有必要通过将聚集查询包装在ExpressionWrapper语句中来划定其范围。output_field.-当一个聚合查询由多个模型字段组成,并且数据类型不同时,需要用一个模型数据类型指定output_field。在清单 8-54 中,因为stock是一个IntegerField模型字段,而price是一个FloatField模型字段,所以output_field告诉 Django 将聚合的assets字段生成为FloatField,从而避免数据类型不明确。
SQL 函数查询:Func 表达式和 Django 数据库函数
Func 表达式是 Django 模型支持的另一个表达式子集,它与其他 SQL 表达式的目的相同:使用数据库来执行操作,而不是获取数据,然后在数据库外执行操作(即在 Python 中)。
Django 中使用 Func 表达式来触发数据库函数的执行。与用于对模型字段执行基本操作的 F 表达式不同,Func 表达式用于执行数据库支持的更复杂的函数,并对模型字段运行这些函数。
清单 8-55 展示了一个调用 SQL 函数的 Func 表达式的例子,以及几个模拟 SQL 函数的 Django 数据库函数。
from django.db.models import F, Func, Value
from django.db.models.functions import Upper, Concat
from coffeehouse.stores.models import Store
# SQL Upper function call via Func expression and F expression
stores_w_upper_names = Store.objects.annotate(name_upper=Func(F('name'), function='Upper'))
stores_w_upper_names[0].name_upper
'CORPORATE'
stores_w_upper_names[0].name
'Corporate'
# Equivalent SQL Upper function call directly with Django SQL Upper function
stores_w_upper_names_function = Store.objects.annotate(name_upper=Upper('name'))
stores_w_upper_names_function[0].name_upper
'CORPORATE'
# SQL Concat function called directly with Django SQL Concat function
stores_w_full_address = Store.objects.annotate(full_address=
Concat('address',Value(' - '),'city',Value(' , '),'state'))
stores_w_full_address[0].full_address
'624 Broadway - San Diego, CA'
stores_w_full_address[0].city
'San Diego'
Listing 8-55.Django Func() expressions
for SQL functions and Django SQL functions
清单 8-55 中的第一个例子利用Func()表达式通过annotate()生成额外的name_upper字段。额外的name_upper字段的目的是以大写格式获取所有Store记录的名称,这个过程非常适合 SQL UPPER 函数。在清单 8-55 的情况下,Func()表达式声明了两个参数:一个F表达式指定要应用函数的模型字段,另一个function参数指定要使用的 SQL 函数。一旦创建了查询,您可以在清单 8-55 中看到,每条记录都可以访问带有大写版本的name字段的附加name_upper字段,以及其他模型字段。
虽然Func()表达式是使用 SQL 表达式生成 Django 模型查询的最灵活的选项,但是对于默认场景,Func()表达式可能会很冗长。Django 提供了一种更快的替代方法,通过django.db.models.functions包中的 SQL 函数生成 SQL 表达式。
清单 8-55 中的第二个查询等价于第一个查询,但是注意这个变化使用 Django 数据库Upper()函数作为annotate()方法的参数,类似于 Django 集合类如何在annotate()语句中声明。
清单 8-55 中的第三个例子通过annotate()生成附加的full_address字段,并利用 Django 数据库Concat()函数。Concat()函数的目的是连接多个模型字段的值。在清单 8-55 的情况下,Concat()函数将Store型号的地址、城市和州的值连接起来。为了在串联的字段值之间留出空格,Concat()函数使用 Django Value()表达式输出逐字分隔符和空格。一旦创建了查询,您可以在清单 8-55 中看到,每条记录都可以访问附加的full_address字段以及address, city和state字段的串联值,还可以访问其他模型字段。
Django 在django.db.models.functions包 5 中包含了十几个数据库函数,用于字符串、日期和其他可以在查询中作为 SQL 函数使用的数据类型。
SQL 子查询:子查询表达式
SQL 子查询是嵌套在其他标准 CRUD 查询或包含其他子查询中的查询。大多数 SQL 子查询在两种情况下使用。第一种情况发生在您需要创建包含跨多个表的相关字段的 SQL 查询时,但是基础表之间没有显式的关系。
第一个 SQL 子查询场景常见于涉及多个 Django 模型且缺少关系数据类型(即OneToOneField、ForeignKey和ManyToManyField)的查询。清单 8-56 展示了通过使用Subquery表达式解决的 SQL 子查询场景。
from django.db.models import OuterRef, Subquery
class Order(models.Model):
created = models.DateTimeField(auto_now_add=True)
class OrderItem(models.Model):
item = models.IntegerField()
amount = models.IntegerField()
order = models.ForeignKey(Order)
# Get Items in order number 1
order_items = OrderItem.objects.filter(order__id=1)
# Get item
order_items[0].item
1
# Get item name ?
# OrderItem item field is IntegerField, lacks Item relationship
# Create sub-query to get Item records with id
item_subquery = Item.objects.filter(id=(OuterRef('id')))
# Annotate previous query with sub-query
order_items_w_name = order_items.annotate(item_name=Subquery(item_subquery.values('name')[:1]))
# Output SQL to verify
print(order_items_w_name.query)
SELECT `online_orderitem`.`id`, `online_orderitem`.`item`,
`online_orderitem`.`amount`, `online_orderitem`.`order_id`,
(SELECT U0.`name` FROM `items_item` U0 WHERE U0.`id` = (online_orderitem.`id`) LIMIT 1)
AS `item_name` FROM `online_orderitem` WHERE `online_orderitem`.`order_id` = 1
# Access item and item_name
order_items_w_name[0].item
1
order_items_w_name[0].item_name
'Whole-Grain Oatmeal'
Listing 8-56.Django Subquery expression with SQL subquery to get related model data
清单 8-56 中的第一行显示了Order和OrderItem型号,包括一个获取所有属于Order编号 1 的OrderItem记录的查询。接下来,您可以看到虽然OrderItem模型有一个item字段,但是它的值是一个整数。这就产生了一个问题,因为不可能获得与一个item字段整数值相关联的name和其他属性,或者换句话说,OrderItem记录缺少与项目模型的关系。这个问题可以通过子查询来解决。
接下来在清单 8-56 中,声明了Item.objects.filter(id=(OuterRef('id')))子查询以通过id获取所有的Item记录,这是主OrderItem期望映射到item值的值。特殊的OuterRef语法像一个F表达式一样工作,直到父查询被解析;毕竟,要通过id获得的Item记录依赖于父查询(例如,对于OrderItem记录中的那些项目,子查询应该只通过id获得Item记录)。
一旦在清单 8-56 中定义了子查询,它就会通过annotate()方法和Subquery()表达式链接到初始的OrderItem查询。接下来,您可以看到查询生成的 SQL 包含一个引用Item模型的子查询。最后,清单 8-56 展示了通过子查询生成的OrderItem查询上附加的item_name字段的输出。
涉及子查询的第二种情况是,一个 SQL 查询必须生成一个 WHERE 语句,该语句的值依赖于另一个 SQL 查询的结果。这个场景在清单 8-57 中进行了说明。
# See Listing 8-56 for referenced model definitions
from coffeehouse.online.models import Order
from coffeehouse.items.models import Item
from django.db.models import OuterRef, Subquery
# Get Item records in lastest Order to replenish stock
most_recent_items_on_order = Order.objects.latest('created').orderitem_set.all()
# Get a list of Item records based on recent order using a sub-query
items_to_replenish = Item.objects.filter(id__in=Subquery(
most_recent_items_on_order.values('item')))
print(items_to_replenish.query)
SELECT `items_item`.`id`, `items_item`.`menu_id`, `items_item`.`name`, `items_item`.`description`, `items_item`.`size`, `items_item`.`calories`,
`items_item`.`price`, `items_item`.`stock` FROM `items_item` WHERE `items_item`.`id`
IN (SELECT U0.`item` FROM `online_orderitem` U0 WHERE U0.`order_id` = 1)
Listing 8-57.Django Subquery expression with SQL subquery in WHERE statement
清单 8-57 的第一步是从最新的Order记录中获取所有的OrderItem记录,目的是检测要补充哪个Item库存。然而,因为OrderItem记录使用一个普通的整数 id 来引用Item记录,所以有必要创建一个子查询来获取基于OrderItem整数引用的所有Item记录。
接下来在清单 8-57 中,查询 id 包含在子查询中的项目记录。在这种情况下,Subquery表达式用于指向most_recent_items_on_order查询,该查询仅从最近的Order记录中获取item值(即整数值)。
最后,清单 8-57 展示了生成的查询如何使用利用子查询的 WHERE 语句。
Join Queries
SQL JOIN 关键字用于从多个数据库表中产生查询。Django 通过 select_related()方法支持相关模型的连接查询,这在前面的跨 Django 模型的 CRUD 关系记录中有所描述。
如果您想在没有 Django 模型关系的表之间创建一个连接查询,您可以使用一个原始 SQL 查询,这将在下一节中介绍。
使用原始(开放式)SQL 对查询进行建模
尽管 Django 模型查询非常广泛,但是在某些情况下,前面几节中介绍的选项都不足以执行某些 CRUD 操作。在这种情况下,您必须依赖原始(开放式)SQL 查询,这是对连接到 Django 项目的数据库执行操作的最灵活的方法。
Django 提供了两种执行原始 SQL 查询的方法。第一种方法是使用模型管理器的raw()方法,将得到的 SQL 查询数据放入 Django 模型中,这样做的额外好处是原始 SQL 查询的行为尽可能接近本地 Django 模型查询。第二种方法是使用 Python 数据库连接——由 Django 管理——获取 SQL 查询数据,并使用较低级别的 Python DB API 函数(例如,cursor)对其进行处理。 6
使用模型管理器的 raw()方法的 SQL 查询
模型管理器的raw()方法应该是您执行原始 SQL 查询的第一选择,因为结果被结构化为一个RawQuerySet类实例,这与您到目前为止使用的 Django 模型查询产生的QuerySet类实例非常相似。
因此,RawQuerySet类实例——就像QuerySet类实例一样——提供了一种使用 Django 模型的字段访问记录的简单方法,能够延迟模型字段的加载,以及索引和切片。清单 8-58 展示了用模型管理器的raw()方法执行的一系列原始 SQL 查询。
from coffeehouse.items.models import Drink, Item
# Get all drink
all_drinks = Drink.objects.raw("SELECT * FROM items_drink")
# Confirm type
type(all_drinks)
# Outputs: <class 'django.db.models.query.RawQuerySet'>
# Get first drink with index 0
first_drink = all_drinks[0]
# Get Drink name (via item OneToOne relationship)
first_drink.item.name
# Use parameters to limit a raw SQL query
caffeine_limit = 100
# Create raw() query with params argument to pass dynamic arguments
drinks_low_caffeine = Drink.objects.raw("SELECT * FROM items_drink where caffeine < %s",params=[caffeine_limit]);
Listing 8-58.Django model manager raw() method
清单 8-58 中的第一个代码片段使用Drink模型管理器的raw()方法发出SELECT * FROM items_drink查询。值得一提的是,这个原始查询产生与原生 Django 查询Drink.objects.all()相同的结果,但是与产生QuerySet数据结构的原生查询不同,请注意raw()方法查询是如何产生RawQuerySet数据结构的。
因为RawQuerySet数据结构是QuerySet的子类,清单 8-58 展示了如何使用与QuerySet数据结构相同的机制。例如,对记录的访问也是通过索引来完成的(例如,使用[0]来获取第一个元素),也可以使用点符号来访问相关的模型。
最后,清单 8-58 中的最后一个例子说明了如何使用params参数创建带有动态参数的原始 SQL 查询。在所有需要创建依赖于动态值(例如,由用户或另一个子程序提供)的raw() SQL 查询的情况下,您应该总是创建带有占位符-% s --的原始 SQL 查询字符串,然后通过params参数进行替换。在清单 8-58 的情况下,请注意caffeine_limit变量是如何在params中声明的,以便稍后被替换到原始 SQL 查询中。params参数确保动态值在应用到数据库之前从查询中逸出,避免了潜在的 SQL 注入安全攻击。 7
清单 8-58 中的raw() SQL 例子很简单,因为查询结果直接映射到预期的模型。换句话说,SELECT * FROM items_drink查询产生了 Django 创建Item记录所需的结果,而无需额外的帮助。虽然有时候,raw() SQL 查询需要额外的配置才能创建底层的模型记录。
例如,如果您对一个遗留表或多个表执行原始 SQL 查询,打算使用某个模型的 raw()方法,那么您必须确保 Django 能够将原始 SQL 查询的结果解释给模型,方法是在原始 SQL 中使用 SQL 作为语句,或者依赖于raw() translations 参数。清单 8-59 展示了这两种技术。
# Map results from legacy table into Item model
all_legacy_items = Item.objects.raw("SELECT product_name AS name, product_description AS description from coffeehouse_products")
# Access legacy results as if they are standard Item model records
all_legacy_items[0].name
# Use explicit mapping argument instead of 'as' statements in SQL query
legacy_mapping = {'product_name':'name','product_description':'description'}
# Create raw() query with translations argument to map table results
all_legacy_items_with_mapping = Item.objects.raw("SELECT * from coffeehouse_products", translations=legacy_mapping)
# Deferred model field loading, get item one with limited fields
item_one = Item.objects.raw("SELECT id,name from items_item where id=1")
# Acess model fields not referenced in the raw query, just like QuerySet defer()
item_one[0].calories
item_one[0].price
# Raw SQL query with aggregate function added as extra model field
items_with_inventory = Item.objects.raw("SELECT *, sum(price*stock) as assets from items_item");
# Access extra field directly as part of the model
items_with_inventory[0].assets
Listing 8-59.Django model manager raw() method with mapping, deferred fields, and aggregate queries
清单 8-59 中的第一个例子用多个 SQL AS 语句声明了一个raw()方法,在这种情况下,每个 AS 子句对应一个项目模型字段。以这种方式,当 Django 检查原始查询的结果时,它知道如何将结果映射到Item实例,而不考虑底层数据库表列名。
清单 8-59 中的第二个例子声明了一个带有translations参数的raw()方法,其值是一个 Python 字典,该字典将数据库表列名映射到 Django 模型字段。在这种情况下,当 Django 在原始查询结果中遇到未知的数据库表列名时,它使用translations字典来确定如何将结果映射到条目实例。
清单 8-59 中的第三个例子说明了即使使用raw()方法发出部分原始 SQL 查询,Django 也能够获取缺失的字段,就好像它是本地QuerySet数据结构一样。最后,清单 8-59 中的第四个例子说明了raw()方法如何能够处理声明为聚集查询的额外字段,以及它们如何变得可访问,就好像它们是用本地模型aggregate()方法添加的一样。
使用 Python 的 DB API 进行 SQL 查询
尽管 Django model raw()方法提供了一个创建原始 SQL 查询的很好的替代方法,并且能够利用原生 Django 模型特性,但是在某些情况下,使用模型的raw()方法进行原始 SQL 查询是行不通的。要么是因为原始 SQL 查询的结果不能映射到 Django 模型,要么是因为您只想在不受任何 Django 模型影响的情况下访问原始数据。
在这种情况下,您需要使用第二个 Django 选项来执行原始 SQL 查询,包括直接连接到数据库并显式提取查询结果。尽管第二种 Django 方法在技术上是与数据库交互最灵活的,但它也需要使用来自 Python 的 DB API 的低级调用。
使用这种技术执行原始 SQL 查询时,Django 中唯一可以利用的是 Django 项目中定义的数据库连接(即settings.py中的DATABASES变量)。一旦建立了数据库连接,您将需要依赖 Python DB API 方法,如cursor()、fetchone()和fetchall(),以及执行结果的手动提取,以便能够成功运行原始 SQL 查询。
清单 8-60 展示了在 Django 环境中使用 Python DB API 的 SQL 查询。
from django.db import connection
# Delete record
target_id = 1
with connection.cursor() as cursor:
cursor.execute("DELETE from items_item where id = %s", [target_id])
# Select one record
salad_item = None
with connection.cursor() as cursor:
cursor.execute("SELECT * from items_item where name='Red Fruit Salad'")
salad_item = cursor.fetchone()
# DB API fetchone produces a tuple, where elements are accessible by index
salad_item[0] # id
salad_item[1] # name
salad_item[2] # description
# Select multiple records
all_drinks = None
with connection.cursor() as cursor:
cursor.execute("SELECT * from items_drink")
all_drinks = cursor.fetchall()
# DB API fetchall produces a list of tuples
all_drinks[0][0] # first drink id
Listing 8-60.Django raw SQL queries with connection() and low-level DB API methods
清单 8-60 中的第一条语句导入了django.db.connection,它代表了在settings.py的DATABASES变量中定义的default数据库连接。一旦有了对 Django 数据库的connection引用,就可以开始使用 Python DB API,这通常是从使用cursor()方法开始的。 8
清单 8-60 中的第一个原始 SQL 查询在connection上打开一个cursor,并执行cursor.execute()方法来执行删除操作。因为删除查询不返回结果,所以在调用了cursor.execute()方法之后,操作被认为是结束了
Tip
如果在数据库中声明多个数据库引用,可以使 django.db.connections 引用在特定数据库上创建一个游标,而不是默认的游标:
从 django.db 导入连接
游标=连接['分析']。cursor() # Cursor 连接到“分析”数据库
清单 8-60 中的第二个原始 SQL 查询首先声明了salad_item占位符变量来存储原始 SQL 查询的结果。一旦完成,另一个cursor在connection上打开,使用相同的cursor.execute()方法执行选择操作。因为 select 查询返回一个结果,所以对cursor.fetchone()方法进行了一次额外的调用,以提取查询结果并将它们分配给占位符变量。注意,使用fetchone()方法是因为预计原始 SQL 查询将返回单个记录结果。
接下来,观察索引如何访问salad_item中原始 SQL 查询的结果。因为 Python DB API cursor.fetchone()方法不使用字段名或其他引用,所以您必须知道记录字段返回的顺序,对于包含许多字段的原始 SQL 来说,这个过程可能特别麻烦。
清单 8-60 中的第三个原始 SQL 查询首先声明了all_drinks占位符变量来存储原始 SQL 查询的结果。一旦完成,另一个cursor在connection上打开,使用相同的cursor.execute()方法执行另一个选择操作。因为 select 查询返回一个结果,所以对cursor.fetchall()方法进行了一次额外的调用,以提取查询结果并将它们分配给占位符变量。注意,使用fetchall()方法是因为预计原始 SQL 查询将返回多个记录结果。
接下来,观察多个索引如何访问all_drinks中原始 SQL 查询的结果。因为 Python DB API cursor.fetchall()方法不使用字段名或其他引用,所以第一个索引表示结果中的一条记录,第二个索引表示给定记录中的一个字段值。
模型经理
正如您在本章和上一章的例子中所了解到的,Django 模型的objects参考或默认模型管理器提供了大量执行数据库操作的功能。
在大多数情况下,Django 模型不需要对它们的默认模型管理器或objects引用进行任何修改。但是,在某些情况下,需要定制 Django 模型的默认模型管理器,或者创建多个模型管理器。
定制和多模型管理器
创建自定义 Django 模型管理器的一个主要原因是添加自定义管理器方法,以便更容易地在模型上执行循环查询。
例如,运行像Item.objects.filter(menu__name='Sandwiches')或Item.objects.filter(menu__name='Salads')这样的查询很简单,但是如果您开始一遍又一遍地编写这些相同的查询,这个过程就会变得令人厌倦并且容易出错。对于原始 SQL 查询来说尤其如此,它需要花费更多的时间来编写,并且具有更高的复杂性。
自定义管理器方法允许您编写一次查询作为模型的一部分,然后调用自定义管理器方法(就像其他模型管理器方法一样(例如,all()、filter()、exclude()))来触发查询。清单 8-61 展示了一个带有定制方法的定制模型管理器类,包括一个使用它的模型,以及各种模型管理器调用。
from django.db import models
# Create custom model manager
class ItemMenuManager(models.Manager):
def salad_items(self):
return self.filter(menu__name='Salads')
def sandwich_items(self):
return self.filter(menu__name='Sandwiches')
# Option 1) Override default model manager
class Item(models.Model):
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
...
objects = ItemMenuManager()
# Queries on default custom model manager
Item.objects.all()
Item.objects.salad_items()
Item.objects.sandwich_items()
# Option 2) Create new model manager field and leave default model manager as is
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
...
objects = models.Manager()
menumgr = ItemMenuManager()
# Queries on default and custom model managers
Item.objects.all()
Item.menumgr.salad_items()
Item.menumgr.sandwich_items()
# ERROR Item.objects.salad_items() # 'Manager' object has no attribute 'salad_items'
# ERROR Item.objects.sandwich_items() # 'Manager' object has no attribute 'sandwich_items'
Item.menumgr.all()
Listing 8-61.Django custom model manager with custom manager methods
清单 8-61 中的第一个类是作为定制模型管理器的ItemMenuManager。注意这个类是如何从models.Manager类继承它的行为的,这就是它的模型管理器行为。接下来,ItemMenuManager类声明了两个返回QuerySet结果的方法。注意类方法如何引用self——表示模型类实例——并调用标准模型方法来触发数据库查询。
值得一提的是,定制模型管理器不一定需要使用原生模型查询或返回QuerySet数据结构,定制模型管理器同样可以包含任何逻辑(例如 Python DB API 调用)或返回任何数据结构(例如 Python 元组)。
一旦您有了一个定制的模型管理器,有两个选项可以将它分配给一个模型类。第一个选项,如清单 8-61 所示,包括覆盖一个模型的默认模型管理器objects,并显式地为它指定一个定制模型管理器。一旦完成,您就可以使用相同的objects引用来调用定制模型管理器方法。此外,请注意清单 8-61 中的内容,即使覆盖了默认的模型管理器objects,模型仍然可以访问内置的模型管理器方法(例如all(),因为定制模型从父models.Manager类继承了它的行为。
接下来,清单 8-61 展示了集成定制模型管理器的第二种选择。该选项包括添加一个新的模型字段来引用一个自定义管理器,并保持默认管理器objects不变。在这种情况下,定制模型管理器方法通过新的字段引用(例如,Item.menumgr.salad_items())变得可访问,并且objects引用继续以其默认行为工作。
Tip
当在一个模型中声明多个模型管理器时,可以使用 default_manager_name 元选项设置缺省模型管理器。有关模型元选项的更多详细信息,请参见上一章。
Warning
如果在多管理器模型中没有定义默认的模型管理器,Django 会选择模型中声明的第一个管理器。这可能会在模型操作中产生意外行为,这些操作不能显式地选择模型管理器(例如,dumpdata ),这与可以使用点符号选择模型管理器的查询不同。
自定义模型管理器和带有方法的 QuerySet 类
模型管理器与返回QuerySet数据结构的方法紧密相连。正如您所看到的,几乎所有链接到默认模型管理器objects(例如all()、filter())的方法都会生成QuerySet数据结构。当您创建定制模型管理器时,可以覆盖这些QuerySet方法的默认行为,以及创建您自己的定制QuerySet类和方法。
模型管理器中最重要的QuerySet方法之一是get_queryset()方法,用于定义模型的初始QuerySet或模型管理器的all()方法返回的内容。在定制模型管理器中,get_queryset()方法尤其重要,因为它允许您根据模型管理器的目的过滤最初的QuerySet。
清单 8-62 展示了为get_queryset()方法定义定制逻辑的多个定制模型管理器。
class SanDiegoStoreManager(models.Manager):
def get_queryset(self):
return super(SanDiegoStoreManager, self).get_queryset().filter(city='San Diego')
class LosAngelesStoreManager(models.Manager):
def get_queryset(self):
return super(LosAngelesStoreManager, self).get_queryset().filter(city='Los Angeles')
class Store(models.Model):
name = models.CharField(max_length=30)
...
objects = models.Manager()
sandiego = SanDiegoStoreManager()
losangeles = LosAngelesStoreManager()
# Call default manager all() query, backed by get_queryset() method
Store.objects.all()
# Call sandiego manager all(), backed by get_queryset() method
Store.sandiego.all()
# Call losangeles manager all(), backed by get_queryset() method
Store.losangeles.all()
Listing 8-62.Django custom model managers with custom get_queryset() method
清单 8-62 中的前两个类代表定制模型管理器;然而,请注意,与清单 8-61 中的定制模型管理器不同,SanDiegoStoreManager和LosAngelesStoreManager类都定义了get_queryset()方法。在这两种情况下,get_queryset()方法都返回一个通过调用父模型管理器get_queryset()方法(即all())生成的QuerySet——通过super()方法——并根据定制模型管理器的目的(例如,按城市获取商店)对父模型应用一个额外的filter()。
一旦定义了定制管理器,清单 8-62 将定制模型管理器声明为Store模型类中的独立字段。最后,在清单 8-62 中,您可以看到使用all()方法对每个模型管理器的调用,这些调用根据后台get_queryset()方法的逻辑返回适当的过滤结果。
多个定制模型管理器的替代方法是创建一个定制管理器,并依赖一个定制的QuerySet类和方法来执行相同的逻辑,这种技术在清单 8-63 中进行了说明。
class StoreQuerySet(models.QuerySet):
def sandiego(self):
return self.filter(city='San Diego')
def losangeles(self):
return self.filter(city='Los Angeles')
class StoreManager(models.Manager):
def get_queryset(self):
return StoreQuerySet(self.model, using=self._db)
def sandiego(self):
return self.get_queryset().sandiego()
def losangeles(self):
return self.get_queryset().losangeles()
class Store(models.Model):
name = models.CharField(max_length=30)
...
objects = models.Manager()
shops = StoreManager()
Store.shops.all()
Store.shops.sandiego()
Store.shops.losangeles()
Listing 8-63.Django custom model manager with custom QuerySet class and methods
清单 8-63 中的StoreQuerySet类是一个自定义的QuerySet类,它定义了sandiego()和losangeles()方法,这两个方法都将额外的过滤器应用于其基类QuerySet。一旦有了一个QuerySet类,就有必要将它与一个定制的模型管理器关联起来。在清单 8-63 中,您可以看到StoreManager类表示一个定制的模型管理器,它定义了它的get_queryset()方法来通过定制的StoreQuerySet类设置它的初始数据。
接下来,注意定制模型管理器StoreManager类是如何定义额外的sandiego()和 losangeles()方法的,这两个方法被连接起来以调用定制StoreQuerySet类中同名的方法。
最后,定制模型管理器StoreManager被设置为Store模型类中的shops字段,在这里您可以观察到调用是如何通过shops引用触发定制StoreQuerySet类支持的查询方法的。
尽管清单 8-63 中的技术有助于减少模型管理器的数量,但是如果你仔细查看清单 8-63 ,仍然有相当多的冗余为定制模型管理器和定制QuerySet类声明相似的命名方法。
为了在使用定制模型管理器和定制QuerySet类时减少多余的方法,后一种类型的类提供了as_manager()方法来自动将QuerySet类转换成定制模型管理器,这种技术在清单 8-64 中有所说明。
class StoreQuerySet(models.QuerySet):
def sandiego(self):
return self.filter(city='San Diego')
def losangeles(self):
return self.filter(city='Los Angeles')
class Store(models.Model):
name = models.CharField(max_length=30)
...
objects = models.Manager()
shops = StoreQuerySet.as_manager()
Store.shops.all()
Store.shops.sandiego()
Store.shops.losangeles()
Listing 8-64.Django custom model manager with custom QuerySet converted to manager
清单 8-64 中的例子定义了与清单 8-63 中相同的自定义QuerySet类;但是,请注意缺少一个定制的模型管理器类。相反,清单 8-64 中的Store模型定义直接引用了定制的StoreQuerySet类,并在其上调用as_manager()来将QuerySet类转换成模型管理器。最后,注意通过shops引用进行的调用与清单 8-63 中的调用是相同的。以这种方式,如果您使用定制的QuerySet类,清单 8-64 中的技术为您节省了创建显式定制模型管理器的额外工作。
相关模型的定制反向模型管理器
在前面的 CRUD relationship records 小节中,您了解了相互之间有关系的模型如何使用反向查询或_set语法来执行来自没有关系定义的模型的操作。
这些反向操作由一个名为RelatedManager的模型管理器执行,它是模型默认管理器的子类。这意味着所有的反向查询或_set语法调用都是基于objects模型管理器引用或模型使用的任何默认模型管理器。
如果您在一个模型上配置了一个默认的模型管理器,那么这个模型上的所有反向操作都将自动使用这个管理器。然而,可以为逆向操作专门定义一个定制的模型管理器,而忽略默认的模型管理器。这项技术包括显式声明一个模型管理器作为逆向操作的一部分,如清单 8-65 所示。
from django.db import models
class Item(models.Model):
...
objects = models.Manager() # Default manager for direct queries
reverseitems = CustomReverseManagerForItems() # Custom Manager for reverse queries
# Get Menu record named Breakfast
breakfast_menu = Menu.objects.get(name='Breakfast')
# Fetch all Item records in the Menu, using Item custom model manager for reverse queries
breakfast_menu.item_set(manager='reverseitems').all()
# Call on_sale_items() custom manager method in CustomReverseManagerForItems
breakfast_menu.item_set(manager='reverseitems').on_sale_items()
Listing 8-65.Django custom model manager for reverse query operations
清单 8-65 首先用默认的objects模型管理器和一个分配给reverseitems字段的定制模型管理器声明项目模型。接下来,进行一个查询来获得一个菜单记录,然后通过反向的_set语法进行各种查询来获得Menu记录的相关Item记录。
然而,请注意清单 8-65 中带有_set语法的反向查询操作是如何使用manager参数来指示哪个模型管理器用于反向操作的;在这种情况下,reverseitems模型管理器用于执行查询,而不是默认的objects模型管理器。
Footnotes 1
https://docs.djangoproject.com/en/1.11/ref/models/querysets/#prefetch-objects
2
https://docs.djangoproject.com/en/1.11/howto/custom-lookups/
3
https://docs.djangoproject.com/en/1.11/ref/models/querysets/#extra
4
https://en.wikipedia.org/wiki/Race_condition
5
https://docs.djangoproject.com/en/1.11/ref/models/database-functions/
6
https://www.python.org/dev/peps/pep-0249/
7
https://en.wikipedia.org/wiki/SQL_injection
8
[en.wikipedia.org/wiki/Cursor_(databases)](en.wikipedia.org/wiki/Cursor…