正确理解 Django ORM 中的 _、__、.
一次性梳理 user_id、user.id、user__id
在 Django 代码中,你可能已经见过这些用法。
Post.objects.filter(user_id=1)
Post.objects.filter(user__id=1)
post.user.id
post.user_id
因为出现了下划线 _、双下划线 __、点 . 的混合,容易让人产生混淆。若随意混用,细微的行为差异和性能问题就会随之出现。
本文将覆盖:
- Django ORM 中
_/__/.分别代表什么 user_id/user.id/user__id的 意义与性能差异- 实际工作中 何时使用哪种写法 的准则
1. 大局观:三种符号的角色
先把三者简要归纳:
_(单下划线)- 只是字段名的一部分。
- 例如:
created_at、user_id。 -
在 Django 中,
ForeignKey会自动生成<字段名>_id的实际数据库列。 -
__(双下划线) - Django ORM 专用的“查询分隔符(lookup separator)”。
- 仅在
filter()、exclude()、order_by()、values()等方法的关键字参数名中有意义。 -
例子:
age__gte=20(字段 + 条件)user__email='a@b.com'(跨表访问)created_at__date=...(转换/变换后条件)
-
.(点) - Python 的属性访问运算符。
- 例如
obj.user、obj.user.id、qs.filter(...).order_by(...)等。 - 在 ORM 里,它只是“访问内存中的 Python 对象属性”,必要时会触发额外查询(懒加载)。
接下来从 Django ORM 的角度更详细地看每个符号。
2. _ 与 _id:ForeignKey 周围的含义
2.1 模型定义示例
from django.contrib.auth import get_user_model
from django.db import models
User = get_user_model()
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=100)
这样定义后,数据库大致会有以下列:
post表id(主键)user_id(实际 FK 列名)title
Django 会:
- 在 Python 对象上创建
post.user(User 实例) - 同时创建
post.user_id(整数或 PK 类型)
即:
post.user # -> User 对象(需要时会额外查询)
post.user_id # -> User 的 PK 值(已在内存中,无需额外查询)
2.2 查询中使用 _id
对 user 这个 FK 进行查询时,有多种写法:
# 1) 传入 User 对象
Post.objects.filter(user=request.user)
# 2) 传入 PK 值(同样有效)
Post.objects.filter(user=request.user.id)
# 3) 直接使用 _id 列名
Post.objects.filter(user_id=request.user.id)
这三种写法最终编译为相同的 WHERE 条件(简单 PK 比较)。
小结:
_id是 * “FK 字段在数据库中实际拥有的列名”, * ORM 中“直接使用该列名”的方式。
3. __ 双下划线链:Django ORM 查询分隔符
从这里开始,才是 Django ORM 独有的语法。
官方文档称 __ 为 “ORM lookup separator”。它用于连接字段名、关系、条件或变换。
3.1 基本模式
几乎所有用法可归纳为三种:
字段名__lookup* 例:age__gte=20、name__icontains='kim'关系字段__其他字段* 例:user__email='a@b.com'、profile__company__name='...'字段__transform__lookup* 例:created_at__date=date.today()、created_at__year__gte=2024
示例:
# 1) 简单比较
Post.objects.filter(title__icontains="django")
# 2) 通过关系过滤
Post.objects.filter(user__email__endswith="@example.com")
# 3) 只取日期字段的年份
Post.objects.filter(created_at__year=2025)
当使用 user__email 之类的跨表访问时,数据库层面会产生 JOIN。
核心:出现
__就意味着 ORM 在生成 SQL 时会做更复杂的操作(JOIN / 函数 / 条件)。
4. . 点运算符与 user.id、user_id
现在一起看 user.id 与 user_id。
4.1 Django 实例中的差异
post = Post.objects.first()
post.user # User 对象(需要时会额外查询)
post.user.id # 上面获取的 User 对象的 PK
post.user_id # Post 表中已存在的 FK 值
关键点:
- 第一次访问
post.user时,如果 User 尚未加载,会触发额外查询(懒加载)。 post.user_id已在post对象中,不需要额外查询。- 因此,“只需要 PK 值”时,使用
user_id更轻量。
4.2 性能示例:列表页面的差异
# views.py
posts = Post.objects.all() # 未使用 select_related
for post in posts:
print(post.user.id)
posts的查询:1 次- 在循环中第一次访问
post.user时,每次都会额外查询 User 表 → 100 条 Post 就会导致 100 条 User 查询(典型的 N+1 问题)
相反:
for post in posts:
print(post.user_id)
user_id已在 Post 表中,无需额外查询。
当然,如果使用 select_related 预先 JOIN:
posts = Post.objects.select_related("user")
for post in posts:
print(post.user.id) # 无额外查询
5. user_id、user.id、user__id 的精确比较
现在把三者放在一起对比。
5.1 user_id – 直接使用 DB 列
- 在哪儿?
- 模型实例:
post.user_id - ORM 查询:
filter(user_id=1)、order_by("user_id")等 - 意义
- 直接使用
post表的实际 FK 列user_id。 - 性能
- 实例读取时无额外查询。
- 查询时最终生成
WHERE "post"."user_id" = 1。
5.2 user.id – 通过关系获取 PK
- 在哪儿?
- 仅在 Python 对象中使用(
post.user.id)。 - 意义
- 先获取
post.user(User 对象),再读取其id。 - 性能
- 若 User 未加载,触发额外查询。
- 若使用
select_related("user"),则已在同一查询中获取,无额外查询。
小结: * “只需要值” →
user_id* “需要 User 的其他字段或已使用 select_related” →user.<字段>
5.3 user__id – ORM 查询中的关系条件
- 在哪儿?
- 仅在 ORM 查询方法的关键字参数中出现。
- 例:
Post.objects.filter(user__id=1)。 - 意义
- 通过
userFK 关联到 User 表,再对其id字段做条件。 - 行为
- 逻辑上会产生
JOIN user ON post.user_id = user.id,然后WHERE user.id = 1。 - 实际 SQL
- 在简单情况下,ORM 可能优化为
user_id = 1,但从模型角度仍视为“跨表查询”。
6. _ 与 __ 的易混淆模式整理
6.1 过滤器中常见组合
# 1) 直接使用 FK 列名 (_id)
Post.objects.filter(user_id=1)
# 2) 传入 FK 字段的 PK 值
Post.objects.filter(user=1)
# 3) 通过关系设置条件 (__)
Post.objects.filter(user__id=1)
Post.objects.filter(user__email__icontains="@example.com")
实务中常见的准则:
- 仅用 FK 的 PK 过滤 →
user_id=...或user=request.user - 需要关联模型的其他字段过滤 →
user__email=...、user__is_active=True等,使用__。
6.2 _ 是“字段名的一部分”,__ 是“ORM 运算符”
_(下划线)- 例如
created_at、user_id、is_active等普通名称。 - 在 Python 变量、模型字段、数据库列中均可出现。
__(双下划线)- 仅在 ORM 查询的关键字参数中有特殊意义。
- 用于连接字段、关系、变换、条件。
7. 实务中需要记住的规则总结
-
仅用 FK 值过滤
python Post.objects.filter(user=request.user) Post.objects.filter(user_id=request.user.id)两种都可,团队约定即可。个人倾向于传入对象(user=request.user)以提升可读性。 -
按关联模型的其他字段过滤
python Post.objects.filter(user__is_active=True) Post.objects.filter(user__email__endswith="@example.com")这时__链发挥作用。 -
模板/视图中仅需 FK 值
django {{ post.user.id }} {# 可能触发额外查询 #} {{ post.user_id }} {# 更轻量 #}若未使用select_related("user"),后者可避免 N+1 问题。 -
频繁使用 User 的多个字段
python posts = Post.objects.select_related("user") for post in posts: print(post.user.username, post.user.email)结合select_related与post.user.<字段>是最干净、最省查询的做法。 -
__链会导致 JOIN * 记住:越多的__链,可能越多的 JOIN,尤其是反向 FK 或多级链。 * 过度使用会导致查询膨胀和重复结果。
8. 一句话总结
_:仅是名称,尤其<字段名>_id是 FK 的实际列名。__:ORM 专用运算符,用于连接字段、关系、条件、变换。.:Python 属性访问。user.id先获取 User 对象,再读取其 PK。user_id:Post 表的 FK 值。user.id:User 对象的 PK。user__id:ORM 查询中“通过 user 关系访问 User.id 并做条件”。
准确区分这三者,可让查询意图更清晰,避免不必要的 JOIN 或 N+1 查询,并在团队代码评审中更容易得到“使用 user_id 更合适”的反馈。

目前没有评论。