Django数据库交互ORM实用秘籍

199 阅读6分钟

前言

ORM是一种强大的数据操作工具。对开发人员来说,熟练使用这个工具对日常功能开发将很有帮助。本文将介绍一些常用场景和ORM技巧。如果有所帮助,请点赞关注支持一下,Thanks♪(・ω・)ノ

有疑问或者其他建议,欢迎评论。

PS: 如果想了解ORM的原理可以看下我的另一篇文章:了解ORM框架看这一篇就够了

1、如何查看数据库交互情况

你在使用ORM工具开发时是否碰到以下类似问题?

  • 在 Django ORM 中执行了一个复杂的查询,但是结果不符合预期,你想查看生成的 SQL 查询语句以便调试。
  • 某些页面加载速度较慢,你怀疑是由于某些数据库查询导致的,想了解哪些查询耗费了大量时间。
  • 你编写了一个复杂的查询,但是它返回了空结果集,你想确认查询是否正确地转换为了 SQL。
  • 你的应用执行的某个查询返回了错误的结果,你需要找出是哪个查询导致了问题。

此时你需要一个查看数据库交互的工具。

使用connection.queries

connection.queries 是 Django 中的一个调试工具,它可以用于查看在请求期间发出的所有数据库查询。可以查看当前数据库连接执行的sql语句和执行时间。

PS: connection.queries在Debug=True时才会生效

>>> from django.db import connection
>>> from django.contrib.auth.models import User
>>> User.objects.values('username','id').first()
>>> connection.queries[-1]
{'sql': 'SELECT `auth_user`.`username`, `auth_user`.`id` FROM `auth_user` ORDER BY `auth_user`.`id` ASC  LIMIT 1', 'time': '0.043'}

适用无法使用query方法的场景

>>> from django.contrib.auth.models import User
>>> from django.db.models import Count, Q
>>> print(User.objects.aggregate(
    total_users=Count('id'),
    total_active_users=Count('id', filter=Q(is_active=True)),
).query)
Traceback (most recent call last):
  File "<input>", line 3, in <module>
AttributeError: 'dict' object has no attribute 'query'

>>> print(User.objects.aggregate(
    total_users=Count('id'),
    total_active_users=Count('id', filter=Q(is_active=True)),
))
{'total_users': 232, 'total_active_users': 208}

>>> connection.queries[-1]
{'sql': 'SELECT COUNT(`auth_user`.`id`) AS `total_users`, COUNT(CASE WHEN `auth_user`.`is_active` = 1 THEN `auth_user`.`id` ELSE NULL END) AS `total_active_users` FROM `auth_user`', 'time': '0.042'}

使用django-debug-toolbar工具

相关介绍可参考文章: juejin.cn/post/684490…

这个工具优点是功能强大,相对更重一点,需要做一下配置修改。如果只是对少量ORM语句分析,使用django自带的connection.queries工具也能满足需求。

2、关联对象查询优化

查询关联对象时,使用select_related()prefetch_related()进行数据库查询可以减少额外的数据库交互,提高性能。

先看看优化前的结果: 每次遍历关联对象都会增加一次数据库IO。

>>> from django.contrib.auth.models import Permission
>>> from django.db import connection
>>> len(connection.queries)
0
>>> [p.content_type.id for p in Permission.objects.all()[:10]]
[1, 1, 1, 1, 28, 28, 28, 28, 111, 111]
>>> len(connection.queries) # 遍历10次关联对象就增加了11次数据库IO
11

再对比下使用select_related查询关联对象的效果:遍历关联对象未增加任何数据库IO。

>>> [p.content_type.id for p in Permission.objects.select_related("content_type").all()[:10]]
[1, 1, 1, 1, 28, 28, 28, 28, 111, 111]
>>> len(connection.queries) # 遍历10次关联对象只增加了1次数据库IO
12

同样的 prefetch_related()也可以优化关联对象的查询。两种方法的主要区别在于:

  1. select_related()

    • 用于一对一或多对一关系。
    • 使用 SQL 的 JOIN 操作在单个查询中获取相关对象。
    • 适用于在一个查询中获取主对象及其关联对象的情况。
  2. prefetch_related()

    • 用于多对多或一对多关系。
    • 使用额外的查询来预取关联对象。

3、高级聚合查询

django2.0后支持在聚合函数中应用过滤器条件。这种用法非常强大,它允许在聚合查询中应用更加复杂的条件,而不仅仅是简单地统计所有记录的数量。

>>> from django.contrib.auth.models import User
>>> from django.db.models import Count, Q

>>> print(User.objects.aggregate(
    total_users=Count('id'),
    total_active_users=Count('id', filter=Q(is_active=True)),
))
{'total_users': 232, 'total_active_users': 208}

以上ORM查询等价的sql如下:

'SELECT COUNT(`auth_user`.`id`) AS `total_users`, COUNT(CASE WHEN `auth_user`.`is_active` = 1 THEN `auth_user`.`id` ELSE NULL END) AS `total_active_users` FROM `auth_user`'

4、具名元组namedtuples

阿里开发手册中指出:

【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。 说明:1)增加查询分析器解析成本。2)增减字段容易与resultMap配置不一致。3)无用字段增加网络消耗,尤其是text类型的字段。

因此不推荐以下写法:

>>> from django.contrib.auth.models import Group
>>> print(Group.objects.all().query)
SELECT `auth_group`.`id`, `auth_group`.`name` FROM `auth_group`

那如何指定字段查询?

可以使用values和values_list方法

>>> print(Group.objects.values("name").query)
SELECT `auth_group`.`name` FROM `auth_group`
>>> print(Group.objects.values("name").first())
{'name': 'AI'}
>>> print(Group.objects.values_list("name").first())
('AI',)

更推荐使用以下方式,将查询的结果转换成namedtuples。相对于以上两种方式方便多,而且更简洁,可读性好

>>> rst = Group.objects.values_list("name", named=True).first()
>>> rst.name
'AI'

5、条件表达式

  • 先看个sql中的条件表达式示例:
SELECT
    employee_id,
    first_name,
    last_name,
    salary,
    CASE
        WHEN salary < 3000 THEN 'Low'
        WHEN salary BETWEEN 3000 AND 7000 THEN 'Medium'
        ELSE 'High'
    END AS salary_level
FROM employees;

对应的django orm实现如下:

from django.db.models import Case, When, Value, CharField

# 假设 Employee 是你的模型类
employees = Employee.objects.annotate(
    salary_level=Case(
        When(salary__lt=3000, then=Value('Low')),
        When(salary__gte=3000, salary__lte=7000, then=Value('Medium')),
        default=Value('High'),
        output_field=CharField()
    )
).values('employee_id', 'first_name', 'last_name', 'salary', 'salary_level')

6、创建新字段

extra()annotate()的区别:

extra()

  1. 是较老的查询方法,在较早版本的Django中使用
  2. 允许在查询中添加自定义的SQL片段
  3. 性能相对较低
  4. 不太推荐在新代码中使用
  5. 灵活但不够安全,容易引入SQL注入风险
  6. 主要用于在查询中添加复杂的自定义选择、where条件或排序

annotate()

  1. 是Django推荐的现代查询方法
  2. 使用QuerySet API,更加ORM友好
  3. 性能更好
  4. 类型安全,不易引入SQL注入
  5. 支持更复杂的聚合和计算
  6. 可以使用各种模型函数和表达式
  7. 更加Pythonic和Django推荐的方式

实际使用建议:

  • 尽量使用annotate(),以获得更好的性能和安全性。
  • 对于复杂的自定义查询,可以配合FuncF()Value()等函数
  • 避免使用extra(),除非有特殊兼容性需求

示例对比:

# extra() 方式(不推荐)
queryset = Model.objects.extra(select={'custom_field': 'some_sql_expression'})

# annotate() 方式(推荐)
from django.db.models import F, Value, CharField
queryset = Model.objects.annotate(
    custom_field=F('some_field') + Value('suffix', output_field=CharField())
)

参考链接

juejin.cn/post/684490…