正确理解 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_atuser_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.userobj.user.idqs.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 基本模式

几乎所有用法可归纳为三种:

  1. 字段名__lookup * 例:age__gte=20name__icontains='kim'
  2. 关系字段__其他字段 * 例:user__email='a@b.com'profile__company__name='...'
  3. 字段__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.iduser_id

现在一起看 user.iduser_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_iduser.iduser__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)
  • 意义
  • 通过 user FK 关联到 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_atuser_idis_active 等普通名称。
  • 在 Python 变量、模型字段、数据库列中均可出现。
  • __(双下划线)
  • 仅在 ORM 查询的关键字参数中有特殊意义。
  • 用于连接字段、关系、变换、条件。

7. 实务中需要记住的规则总结

  1. 仅用 FK 值过滤 python Post.objects.filter(user=request.user) Post.objects.filter(user_id=request.user.id) 两种都可,团队约定即可。个人倾向于传入对象(user=request.user)以提升可读性。

  2. 按关联模型的其他字段过滤 python Post.objects.filter(user__is_active=True) Post.objects.filter(user__email__endswith="@example.com") 这时 __ 链发挥作用。

  3. 模板/视图中仅需 FK 值 django {{ post.user.id }} {# 可能触发额外查询 #} {{ post.user_id }} {# 更轻量 #} 若未使用 select_related("user"),后者可避免 N+1 问题。

  4. 频繁使用 User 的多个字段 python posts = Post.objects.select_related("user") for post in posts: print(post.user.username, post.user.email) 结合 select_relatedpost.user.<字段> 是最干净、最省查询的做法。

  5. __ 链会导致 JOIN * 记住:越多的 __ 链,可能越多的 JOIN,尤其是反向 FK 或多级链。 * 过度使用会导致查询膨胀和重复结果。


8. 一句话总结

  • _:仅是名称,尤其 <字段名>_id 是 FK 的实际列名。
  • __:ORM 专用运算符,用于连接字段、关系、条件、变换。
  • .:Python 属性访问。user.id 先获取 User 对象,再读取其 PK。
  • user_idPost 表的 FK 值
  • user.idUser 对象的 PK
  • user__idORM 查询中“通过 user 关系访问 User.id 并做条件”

准确区分这三者,可让查询意图更清晰,避免不必要的 JOIN 或 N+1 查询,并在团队代码评审中更容易得到“使用 user_id 更合适”的反馈。

image