之前简单了解过ORM,Object Relational Mapping,就是用Python编写SQL表操作,每个Model类对应数据库一张表,每个字段(Field)对应表的一列。
具体原理流程如下:我们定义了一个Model——Django启动时读取所有的model.py并在内存里维护一份Model-Table映射关系——执行查询语句——Django生成SQL——执行并封装结果未QuerySet。
N+1查询问题(select_related、prefetch_related):
场景描述:1个用户手上有n个客户。假设我们想查询客户及对应的用户。简单写法如下:
def customer_list_bad(request):
customers = Customer.objects.all()
for customer in customers:
print(f"客户: {customer.name}, 销售: {customer.sales_rep.username}")
return render(request, 'customer_list.html', {'customers': customers})
但是这有个问题,就是customers仅仅获取了客户表的所有信息,但是并没有用户的相关信息。当我们想要知道每个客户对应的销售时,for循环就回去逐个查询访问数据库。结果就是Customer.objects.all() 执行1次查询,获取了n个客户对象。for 循环n次,访问客户的sales_rep.username,Django为此发起n次查询,最终结果就是n+1。
解决方法也很简单,Django中提供了select_related,这个函数可以将相应的关联表一次性获取出来,也就是SQL语句中的join语句。当然,一个表关联的表可能很多,不可能全部都拿出来,所以select_related()括号内可以选择相应的关联表。
def customer_list_good(request):
customers = Customer.objects.select_related('sales_rep').all()
for customer in customers:
print(f"客户: {customer.name}, 销售: {customer.sales_rep.username}")
return render(request, 'customer_list.html', {'customers': customers})
喵喵CRM优化:
喵喵CRM里面的客户列表等地方存在类似问题,如下代码所示,我们加入一个select_related就可以优化了。
# 客户列表
def get_queryset(self):
return Customer.objects.filter(owner=self.request.user) # 只返回当前用户
# 优化后代码
def get_queryset(self):
return Customer.objects.filter(owner=self.request.user).select_related('owner')
不过权限管理略微复杂。整体序列化和视图代码所示:
from rest_framework import serializers
from django.contrib.auth.models import Group, Permission, User
……
class UserSerializer(serializers.ModelSerializer):
# 这儿如果序列化多个用户时候,就会出现n+1,每个用户都去查询一下所在的组。
groups_read = SimpleGroupSerializer(many=True, read_only=True, source='groups')
……
class GroupSerializer(serializers.ModelSerializer):
# 同上,当序列化多个 Group 对象时,每次访问 users 都会触发一次新的查询
users = UserSerializer(many=True, read_only=True, source='user_set')
# 同上,每次访问 permissions 也会触发一次新的查询
permissions = PermissionSerializer(many=True, read_only=True)
# 视图函数
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [IsAuthenticated, CachedModelPermissions]
class GroupViewSet(viewsets.ModelViewSet):
queryset = Group.objects.all()
serializer_class = GroupSerializer
permission_classes = [IsAuthenticated, CachedModelPermissions]
class PermissionViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Permission.objects.all()
serializer_class = PermissionSerializer
permission_classes = [IsAuthenticated, CachedModelPermissions]
这儿涉及多对多的关系,也就是通过一个单独的关系表来表达两个表之间的关系,不能简单的使用select_related。这时候就要掏出prefetch_related了。prefetch_related执行针对关联表的一个大查询,用 WHERE field_id IN (list_of_ids) 的方式一次性获取所有关联数据 (1 次)。在 Python 内存中将关联数据高效地“拼装”到主对象上。
优化后代码如下:
class UserViewSet(viewsets.ModelViewSet):
# queryset = User.objects.all()
queryset = User.objects.all().prefetch_related('groups') # 优化
serializer_class = UserSerializer
permission_classes = [IsAuthenticated, CachedModelPermissions]
class GroupViewSet(viewsets.ModelViewSet):
# queryset = Group.objects.all()
queryset = Group.objects.all().prefetch_related('user_set', 'permissions') # 优化
serializer_class = GroupSerializer
permission_classes = [IsAuthenticated, CachedModelPermissions]
这个时候我们看下打开权限管理页面的组-权限查询,所执行的SQL语句:
[SQL 查询次数] /permission/groups/: 17
SELECT VERSION(),
@@sql_mode,
@@default_storage_engine,
@@sql_auto_is_null,
@@lower_case_table_names,
CONVERT_TZ('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL
0.000
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED 0.000
SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_u
ser`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1 LIMIT 21 0.001
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` 0.001
SELECT (`auth_user_groups`.`group_id`) AS `_prefetch_related_val_group_id`, `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`las
t_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`,
`auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` INNER JOIN `auth_user_groups` ON (`auth_user`.`id` = `auth_user_groups`.`user_id`) WHERE `auth_user_groups`.`group_id` IN (4, 9, 1, 3, 2) 0.002
SELECT (`auth_group_permissions`.`group_id`) AS `_prefetch_related_val_group_id`, `auth_permission`.`id`, `auth_permission`.`name`, `a
uth_permission`.`content_type_id`, `auth_permission`.`codename` FROM `auth_permission` INNER JOIN `auth_group_permissions` ON (`auth_p
ermission`.`id` = `auth_group_permissions`.`permission_id`) INNER JOIN `django_content_type` ON (`auth_permission`.`content_type_id` =
`django_content_type`.`id`) WHERE `auth_group_permissions`.`group_id` IN (4, 9, 1, 3, 2) ORDER BY `django_content_type`.`app_label` ASC, `django_content_type`.`model` ASC, `auth_permission`.`codename` ASC 0.005
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` = 1 0.001
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` = 2 0.001
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` = 1 0.001
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` = 3 0.001
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` = 30 0.000
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` = 31 0.000
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` = 32 0.000
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` = 33 0.000
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` = 34 0.000
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` = 35 0.000
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` = 2 0.001
可以看到前面的一部分已经修正了,通过WHERE `auth_user_groups`.`group_id` IN (4, 9, 1, 3, 2) 0.002来减少了数据库的访问次数。不过后面还有一大堆查询,看起来是在查询用户所在的组和组名。
这是啥原因了,就是嵌套多了导致的……当我们GET /permission/groups/,GroupViewSet 开始工作。GroupViewSet 使用了优化后的 queryset,它预加载了 user_set 和 permissions。到这里都很好。DRF 开始使用 GroupSerializer 来序列化每个 Group 对象。当 GroupSerializer 序列化到 users 字段时,它会为每个 Group 里的每一个 User 调用 UserSerializer。现在轮到 UserSerializer 工作了。它在序列化每个 User 对象时,看到了 groups_read 字段。UserSerializer 尝试访问这个 User 对象的 .groups 属性来获取其群组列表。
关键点来了:我们对 GroupViewSet 的预加载,只加载了 group.user_set,但并没有为 user_set 里的每一个 user 再去预加载 user.groups。因此,DRF 只能为每个用户单独发起一次数据库查询来获取他的群组列表,从而导致了 N 次查询。
修改如下,Prefetch就是告诉这个Group视图,当你执行user_set查询的时候,不要使用默认的 User.objects.all()。请使用我提供给你的这个 queryset:User.objects.prefetch_related(‘groups’)。这个 queryset 本身就已经包含了对 groups 的预加载。
from django.db.models import Prefetch
class GroupViewSet(viewsets.ModelViewSet):
# queryset = Group.objects.all()
# queryset = Group.objects.all().prefetch_related('user_set', 'permissions') # 优化
# 再优化
queryset = Group.objects.prefetch_related('permissions',
Prefetch('user_set',
queryset=User.objects.prefetch_related('groups')))
serializer_class = GroupSerializer
permission_classes = [IsAuthenticated, CachedModelPermissions]
最后结果
[SQL 查询次数] /permission/groups/: 6
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED 0.000
SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_u
ser`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1 LIMIT 21 0.001
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` 0.001
SELECT (`auth_group_permissions`.`group_id`) AS `_prefetch_related_val_group_id`, `auth_permission`.`id`, `auth_permission`.`name`, `a
uth_permission`.`content_type_id`, `auth_permission`.`codename` FROM `auth_permission` INNER JOIN `auth_group_permissions` ON (`auth_p
ermission`.`id` = `auth_group_permissions`.`permission_id`) INNER JOIN `django_content_type` ON (`auth_permission`.`content_type_id` SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` 0.001
SELECT (`auth_group_permissions`.`group_id`) AS `_prefetch_related_val_group_id`, `auth_permission`.`id`, `auth_permission`.`name`, `auth_permission`.`content_type_id`, `auth_permission`.`codename` FROM `auth_permission` INNER JOIN `auth_group_permissions` ON (`auth_permission`.`id` = `auth_group_permissions`.`permission_id`) INNER JOIN `django_content_type` ON (`auth_permission`.`content_type_id` = `django_content_type`.`id`) WHERE `auth_group_permissions`.`group_id` IN (4, 9, 1, 3, 2) ORDER BY `django_content_type`.`app_label` ASC, `django_content_type`.`model` ASC, `auth_permission`.`codename` ASC 0.001
SELECT (`auth_user_groups`.`group_id`) AS `_prefetch_related_val_group_id`, `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` INNER JOIN `auth_user_groups` ON (`auth_user`.`id` = `auth_user_groups`.`user_id`) WHERE `auth_user_groups`.`group_id` IN (4, 9, 1, 3, 2) 0.004
SELECT (`auth_user_groups`.`user_id`) AS `_prefetch_related_val_user_id`, `auth_group`.`id`, `auth_group`.`name` FROM `auth_group` INNER JOIN `auth_user_groups` ON (`auth_group`.`id` = `auth_user_groups`.`group_id`) WHERE `auth_user_groups`.`user_id` IN (1, 2, 3, 30, 31, 32, 33, 34, 35) 0.001
聚合函数使用
鉴于目前喵喵CRM还没有看版或者统计,所以这儿仅仅列一下使用方法和示例。
from django.db.models import Count
qs = Customer.objects.annotate(order_count=Count('orders')).order_by('-order_count')[:20]
for c in qs:
print(c.name, c.order_count)
事务
同上。select_for_update() + transaction.atomic()。select_for_update是一个悲观锁,必须在事务内使用。
from django.db import transaction
def transfer(from_account_id, to_account_id, amount):
with transaction.atomic():
a = Account.objects.select_for_update().get(id=from_account_id)
b = Account.objects.select_for_update().get(id=to_account_id)
if a.balance < amount:
raise ValueError('余额不足')
a.balance -= amount
b.balance += amount
a.save()
b.save()
# 装饰器也可以
from django.db import transaction
@transaction.atomic
def transfer_funds(from_account, to_account, amount):
# ... 数据库操作 ...