← 返回题目列表

Django 的 Manager 和 QuerySet 有什么区别?怎么自定义?

中等 第 16 / 27 题 更新于 2026/08/01
DjangoManagerQuerySet软删除

简化版

Manager 是「模型访问数据库的入口」(就是那个 objects),QuerySet 是「一次查询的构建器」——Model.objects 是 Manager,而 Model.objects.filter(...) 返回的是 QuerySet。两者的关键区别在于可链式性QuerySet 的方法返回新的 QuerySet,所以能一直 .filter().exclude().order_by() 串下去;而 Manager 的方法只能作为「起点」调用一次Article.objects.published() 之后如果 published() 定义在 Manager 上且返回 QuerySet,还能继续链,但 qs.published() 是不行的)。所以自定义查询逻辑的正确做法是:把方法写在 QuerySet 子类上,再用 Manager.from_queryset(MyQuerySet)MyQuerySet.as_manager() 把它们暴露成 Manager 方法——这样既能 Article.objects.published() 也能 Article.objects.filter(...).published().recent()两端都能链式调用最常见的用途是软删除和多租户:定义一个 get_queryset() 里默认 filter(is_deleted=False) 的 Manager,让业务代码「自动」看不到已删除的数据。但这里有个必须知道的坑覆盖 get_queryset() 做默认过滤会影响关联查询——Django 在跟随外键做反向关联、以及 dumpdata、删除级联时使用的是 _base_manager(默认是第一个定义的 Manager),如果它带了过滤,关联对象可能「凭空消失」甚至导致数据不一致。官方建议:_base_manager(即第一个定义的 Manager)不要做任何过滤。核心记忆:方法写在 QuerySet 上、用 from_queryset 暴露第一个 Manager 不要过滤(关联和级联会用它)。

详细版

Manager 与 QuerySet 对比

ManagerQuerySet
是什么模型的数据库访问入口objects一次查询的构建器
从哪来类属性 objects = Manager()Manager 的方法返回
可链式只能作起点方法返回新 QuerySet
惰性——不求值直到迭代
自定义方法只能在 objects. 后调用一次可以任意串联
最佳实践from_queryset() 生成把查询逻辑都写这里
from django.db import models

# ① ★把查询方法写在 QuerySet 上(可链式)★
class ArticleQuerySet(models.QuerySet):
    def published(self):
        return self.filter(status="published", published_at__lte=timezone.now())
    def by_author(self, user):
        return self.filter(author=user)
    def recent(self, days=7):
        return self.filter(created__gte=timezone.now() - timedelta(days=days))
    def with_stats(self):
        return self.annotate(comment_count=models.Count("comments"))

# ② ★两种暴露方式★
class Article(models.Model):
    # 方式 A:as_manager(★最简洁★)
    objects = ArticleQuerySet.as_manager()

    # 方式 B:from_queryset(★需要再加 Manager 专属方法时用★)
    # objects = models.Manager.from_queryset(ArticleQuerySet)()

# ③ ★效果:两端都能链式★
Article.objects.published()                          # ✓ Manager 起点
Article.objects.published().recent().by_author(u)     # ✓ ★任意串联★
Article.objects.filter(x=1).published().with_stats()  # ✓ 中间也能用

# ④ ★反面:只写在 Manager 上(不能链式)★
class BadManager(models.Manager):
    def published(self):
        return self.filter(status="published")
class Article(models.Model):
    objects = BadManager()
Article.objects.published()                  # ✓
Article.objects.filter(x=1).published()      # ✗ ★AttributeError(QuerySet 没有这个方法)★

# ⑤ ★软删除:覆盖 get_queryset★
class SoftDeleteQuerySet(models.QuerySet):
    def delete(self):                        # ★覆盖批量删除★
        return self.update(is_deleted=True, deleted_at=timezone.now())
    def alive(self):
        return self.filter(is_deleted=False)
    def dead(self):
        return self.filter(is_deleted=True)

class SoftDeleteManager(models.Manager.from_queryset(SoftDeleteQuerySet)):
    def get_queryset(self):
        return super().get_queryset().filter(is_deleted=False)   # ★默认过滤★

class Article(models.Model):
    is_deleted = models.BooleanField(default=False, db_index=True)
    deleted_at = models.DateTimeField(null=True, blank=True)

    # ★★顺序很关键:第一个 Manager 会成为 _base_manager★★
    objects = SoftDeleteManager()            # ⚠️ 见下方警告
    all_objects = models.Manager()           # ★不过滤的后备★

    def delete(self, *args, **kwargs):       # ★覆盖单对象删除★
        self.is_deleted = True
        self.deleted_at = timezone.now()
        self.save(update_fields=["is_deleted", "deleted_at"])

# ⑥ ★Meta.base_manager_name / default_manager_name★
class Article(models.Model):
    all_objects = models.Manager()           # ★第一个 = 不过滤★
    objects = SoftDeleteManager()
    class Meta:
        base_manager_name = "all_objects"    # ★显式指定关联查询用哪个★
        default_manager_name = "objects"     # 表单/admin 等用哪个

⚠️ 三个必须记住的点:① 查询方法要写在 QuerySet 上而不是 Manager。写在 Manager 上的方法只能作为链的起点调用一次——Article.objects.published() 可以,但 Article.objects.filter(x=1).published() 会抛 AttributeError(因为 filter() 返回的是 QuerySet,它没有这个方法)。正确做法是把方法定义在 QuerySet 子类上,然后用 MyQuerySet.as_manager()(最简洁)或 Manager.from_queryset(MyQuerySet)(需要额外的 Manager 专属方法时用)把它们暴露出来——这样两端都能链式调用。② _base_manager 默认是「类里第一个定义的 Manager」,而 Django 在跟随关联、级联删除、dumpdata 等场景使用它。如果第一个 Manager 带了过滤(比如软删除的 filter(is_deleted=False)),会导致:通过外键访问关联对象时它「凭空消失」comment.article 可能抛 DoesNotExist)、级联删除漏掉记录、序列化导出不完整。Django 官方明确建议:_base_manager 不要过滤任何行——把不过滤的 Manager 放在第一位,或用 Meta.base_manager_name 显式指定。③ 软删除要同时覆盖两处 delete()Model.delete()(单个对象)和 QuerySet.delete()(批量),否则 Article.objects.filter(...).delete() 会真的把数据删掉。而且软删除会让唯一约束失效(已删除的记录仍占着唯一值),需要配合条件唯一约束 UniqueConstraint(condition=Q(is_deleted=False))

完整版教学

一、Manager 与 QuerySet 的关系

★ 三层关系:
  ┌──────────────────────────────────────────────────┐
  │ Model.objects        ← ★Manager 实例★(类属性)    │
  │   .filter(...)       ← ★返回 QuerySet★            │
  │     .exclude(...)    ← ★返回新的 QuerySet★        │
  │       .order_by(...) ← 仍然是 QuerySet             │
  │         → 迭代/切片/len 时才★真正执行 SQL★         │
  └──────────────────────────────────────────────────┘

★ Manager 做的事其实很少:
  class Manager:
      def get_queryset(self):
          return QuerySet(self.model, using=self._db)   # ★创建一个新 QuerySet★
      # 其余方法(filter/exclude/all/get...)都是转发给 get_queryset()
      def filter(self, *a, **kw):
          return self.get_queryset().filter(*a, **kw)
  → ★Manager ≈ "QuerySet 工厂 + 一层转发"★

★ 为什么 Django 要分成两个概念:
  ① ★命名空间隔离★:模型实例上不该有 filter/all
     article.filter(...)   # ✗ 没有意义
     Article.objects.filter(...)  # ✓
     (Django 故意★不把 Manager 暴露在实例上★——访问 instance.objects 会报错)
  ② ★可以定义多个入口★:objects / published_objects / all_objects
  ③ ★关联管理器★:article.comments 也是一个 Manager(RelatedManager)

★ 自动创建的规则:
  - 没定义任何 Manager → Django ★自动加一个 objects = Manager()★
  - ★一旦定义了任何 Manager,Django 就不再自动添加 objects★
    class Article(models.Model):
        published = PublishedManager()
    Article.objects   # ✗ ★AttributeError!★
  ✓ 要保留 objects 就显式写出来

★ 关联管理器(RelatedManager):
  article.comments.all()          # 反向外键
  article.tags.add(t)             # 多对多(★有 add/remove/set/clear★)
  ★ 它是★动态创建★的 Manager,基于 _default_manager 的类
  → ★所以自定义 Manager 的方法在关联访问时也可用★:
    article.comments.published()  # ✓ 如果 Comment 的 Manager 有 published

Manager 是「QuerySet 工厂 + 一层转发」——它的 filter/exclude/all 等方法其实都是转发给 get_queryset() 创建的 QuerySet。Django 分成两个概念有三个理由:命名空间隔离(模型实例上不该有 filter,Django 故意不把 Manager 暴露在实例上)、可以定义多个入口、以及关联管理器article.comments 也是一个 Manager)。有个容易踩的规则:一旦你定义了任何 Manager,Django 就不再自动添加 objects——所以自定义时如果还想用 objects,必须显式写出来。另外关联管理器是基于 _default_manager 的类动态创建的,所以你自定义的 Manager 方法在关联访问时也能用article.comments.published())——这是个很实用的特性。

二、自定义 QuerySet:让方法可以链式

★ 核心原则:★查询逻辑写在 QuerySet 上★

  class ArticleQuerySet(models.QuerySet):
      def published(self):
          return self.filter(status="published")      # ★返回 self.filter(...)★
      def recent(self, days=7):
          return self.filter(created__gte=now() - timedelta(days=days))
      def popular(self):
          return self.annotate(n=Count("likes")).filter(n__gte=100)

  ★ 关键:每个方法都 ★return self.filter/annotate/...★
    → 返回的仍是 QuerySet → ★可以继续链★

★ 两种暴露方式:
  ① ★as_manager()(最简洁)★
     class Article(models.Model):
         objects = ArticleQuerySet.as_manager()
     → Django 自动生成一个 Manager,把 QuerySet 的★公开方法★都复制过来

  ② ★from_queryset()(需要 Manager 专属方法时)★
     class ArticleManager(models.Manager.from_queryset(ArticleQuerySet)):
         def create_draft(self, **kw):        # ★只在 Manager 上有意义的方法★
             return self.create(status="draft", **kw)
         def get_queryset(self):              # ★需要改默认查询集时★
             return super().get_queryset().select_related("author")
     class Article(models.Model):
         objects = ArticleManager()

★ 哪些方法会被复制到 Manager:
  ✓ ★所有不以下划线开头的方法★
  ✗ 标记了 queryset_only = True 的不会:
      def dangerous(self): ...
      dangerous.queryset_only = True          # ★只在 QuerySet 上可用★
  ★ 典型:delete()、update() 这类"只应作用于已过滤集合"的方法

★ 组合的威力(★这是自定义 QuerySet 的最大价值★):
  Article.objects.published().recent(30).popular().select_related("author")
  → ★可读性极高、逻辑复用、每段都可单独测试★
  → 对比把条件散落在各个视图里:
    Article.objects.filter(status="published",
                           created__gte=...,
                           ...)                # ★重复、易错、改规则要改多处★

★ 实用的 QuerySet 方法模式:
  ① ★过滤类★:published() / active() / for_user(u) / in_tenant(t)
  ② ★注解类★:with_stats() / with_author()
  ③ ★排序类★:newest() / by_popularity()
  ④ ★聚合类★(终结):total_revenue() → 返回数字而不是 QuerySet
  ⑤ ★批量操作★:publish_all() → self.update(status="published")

★ 一个完整例子:
  class OrderQuerySet(models.QuerySet):
      def paid(self):        return self.filter(status="paid")
      def unpaid(self):      return self.filter(status="pending")
      def for_user(self, u): return self.filter(user=u)
      def this_month(self):
          start = timezone.now().replace(day=1, hour=0, minute=0, second=0)
          return self.filter(created__gte=start)
      def with_items(self):  return self.prefetch_related("items")
      def total_amount(self):                         # ★终结方法★
          return self.aggregate(t=Sum("amount"))["t"] or 0

  # 使用
  Order.objects.for_user(u).paid().this_month().total_amount()
  # → ★读起来就是业务语言★

核心原则是「查询逻辑写在 QuerySet 上」,每个方法都 return self.filter(...) 保证返回的仍是 QuerySet、可以继续链。暴露方式有两种:as_manager() 最简洁(自动把 QuerySet 的公开方法复制到 Manager),from_queryset() 用于还需要 Manager 专属方法或要覆盖 get_queryset() 的场景。有个细节:标记了 queryset_only = True 的方法不会被复制到 Manager(典型的是 delete()update() 这类「只应作用于已过滤集合」的方法)。自定义 QuerySet 的最大价值是组合Order.objects.for_user(u).paid().this_month().total_amount() 读起来就是业务语言,而且每段逻辑只定义一次、可以单独测试——对比把 filter(status="paid", created__gte=...) 散落在各个视图里,后者重复、易错、改规则要改多处。方法可以分五类:过滤类、注解类、排序类、聚合类(终结方法,返回数字)、批量操作类。

三、覆盖 get_queryset 与 _base_manager 的陷阱

★ 默认过滤的写法:
  class PublishedManager(models.Manager):
      def get_queryset(self):
          return super().get_queryset().filter(status="published")
  class Article(models.Model):
      objects = models.Manager()          # ★不过滤(第一个)★
      published = PublishedManager()      # 只看已发布

★ ★_default_manager 和 _base_manager 的区别(★核心考点★)★:
  ┌────────────────┬──────────────────────────────────────────┐
  │ _default_manager│ ★类里第一个定义的 Manager★(可用 Meta 改)  │
  │                 │ 用于:admin、ModelForm、序列化、           │
  │                 │       ★关联管理器的基类★                  │
  ├────────────────┼──────────────────────────────────────────┤
  │ _base_manager   │ ★默认也是第一个 Manager★(可用 Meta 改)    │
  │                 │ 用于:★跟随外键取关联对象★、               │
  │                 │       ★级联删除★、dumpdata                │
  └────────────────┴──────────────────────────────────────────┘
  ★ 两者默认都指向"第一个定义的 Manager"

★ ★为什么"第一个 Manager 不能过滤"★(官方建议):
  class Article(models.Model):
      objects = SoftDeleteManager()       # ★第一个,带 filter(is_deleted=False)★

  后果:
  ① ★跟随外键时对象"消失"★
     comment.article                       # ★如果文章被软删除 → DoesNotExist★
     → Django 用 _base_manager 加载关联对象
     → 你只是想读关联,却因为过滤而报错
  ② ★级联删除漏记录★
     删除 User → 级联删 Article → ★被软删除的文章不会被处理★
  ③ ★dumpdata 导出不完整★
  ④ ★admin 里的关联下拉框缺项★

  ✓ 正确做法(两种):
    A. ★把不过滤的 Manager 放在第一位★
       class Article(models.Model):
           all_objects = models.Manager()        # ★第一个,不过滤★
           objects = SoftDeleteManager()         # 业务默认用这个
           class Meta:
               default_manager_name = "objects"  # ★admin/表单用 objects★
    B. ★用 Meta.base_manager_name 显式指定★
       class Meta:
           base_manager_name = "all_objects"

★ 关联管理器用哪个:
  article.comments.all()
  → 用的是 ★Comment._default_manager 的类★(不是 _base_manager)
  → ★所以 Comment 的默认过滤会生效★
  ★ 这有时是你想要的(自动过滤软删除的评论),
    有时不是(想看全部)→ 用 Comment.all_objects.filter(article=article)

★ 另一个陷阱:★Meta.ordering 与 get_queryset 的排序★
  在 get_queryset 里加 order_by 会覆盖后续的排序意图
  ✓ 排序放在专门的 QuerySet 方法里,让调用方决定

★ 什么时候该用默认过滤:
  ✓ ★软删除★(几乎所有查询都不该看到已删除的)
  ✓ ★多租户★(自动加 tenant 过滤,防止越权)
  ✗ "常用条件"(如 published)→ ★用命名的 QuerySet 方法更清晰★
    因为默认过滤是★隐式★的,新人容易困惑"数据去哪了"

_default_manager_base_manager 的区别是核心考点:前者用于 admin、ModelForm、序列化和关联管理器的基类,后者用于跟随外键取关联对象、级联删除、dumpdata——两者默认都指向「类里第一个定义的 Manager」。这就引出了 Django 官方的明确建议:_base_manager(第一个 Manager)不要做任何过滤,否则会有四个后果:跟随外键时关联对象「凭空消失」comment.articleDoesNotExist)、级联删除漏记录dumpdata 导出不完整、admin 关联下拉框缺项。正确做法是把不过滤的 Manager 放在第一位(配 Meta.default_manager_name 指定业务默认用哪个),或用 Meta.base_manager_name 显式指定。还要注意关联管理器用的是 _default_manager 的类,所以默认过滤在 article.comments.all() 上会生效。最后一个判断:默认过滤适合软删除和多租户这类「几乎所有查询都需要」的场景,而「常用条件」(如 published)用命名的 QuerySet 方法更清晰——因为默认过滤是隐式的,新人容易困惑「数据去哪了」。

四、软删除的完整实现

★ 软删除要处理的六件事(★缺一不可★):
  ① 模型字段
  ② ★覆盖 Model.delete()★(单对象)
  ③ ★覆盖 QuerySet.delete()★(批量)
  ④ ★默认过滤的 Manager★
  ⑤ ★保留一个不过滤的 Manager(放第一位)★
  ⑥ ★条件唯一约束★

  class SoftDeleteQuerySet(models.QuerySet):
      def delete(self):                                   # ★③ 批量★
          return self.update(is_deleted=True, deleted_at=timezone.now())
      def hard_delete(self):                              # 真删除
          return super().delete()
      def alive(self):   return self.filter(is_deleted=False)
      def dead(self):    return self.filter(is_deleted=True)

  class SoftDeleteManager(models.Manager.from_queryset(SoftDeleteQuerySet)):
      def get_queryset(self):                             # ★④ 默认过滤★
          return super().get_queryset().filter(is_deleted=False)

  class Article(models.Model):
      title = models.CharField(max_length=200)
      slug = models.SlugField()
      is_deleted = models.BooleanField(default=False, db_index=True)   # ★①★
      deleted_at = models.DateTimeField(null=True, blank=True)

      all_objects = models.Manager()          # ★⑤ 第一个,不过滤(_base_manager)★
      objects = SoftDeleteManager()

      class Meta:
          default_manager_name = "objects"
          constraints = [                      # ★⑥ 条件唯一★
              models.UniqueConstraint(fields=["slug"],
                                      condition=models.Q(is_deleted=False),
                                      name="uniq_alive_slug"),
          ]

      def delete(self, using=None, keep_parents=False):   # ★②★
          self.is_deleted = True
          self.deleted_at = timezone.now()
          self.save(update_fields=["is_deleted", "deleted_at"])

      def restore(self):
          self.is_deleted = False
          self.deleted_at = None
          self.save(update_fields=["is_deleted", "deleted_at"])

★ 软删除的六个隐藏成本(★决定要不要用之前想清楚★):
  ① ★唯一约束失效★ → 必须用条件唯一(见上)
  ② ★外键关联的一致性★
     文章被软删除,但它的评论还"活着"→ ★需要级联软删除吗?★
     → CASCADE 对软删除★不生效★(因为没有真的 DELETE)
  ③ ★查询性能★:每个查询都多一个条件 → ★is_deleted 要加索引★
     更好:★条件索引 Index(condition=Q(is_deleted=False))★
  ④ ★数据膨胀★:删除的数据永远占空间 → 需要归档策略
  ⑤ ★统计口径★:count() 要不要算已删除的?(容易出错)
  ⑥ ★第三方包不认识★:DRF、admin、导出工具可能绕过你的 Manager

★ 什么时候不该用软删除:
  ✗ 只是"怕删错" → ★用备份和审计日志更合适★
  ✗ 数据量极大且删除频繁 → 膨胀严重
  ✗ 有合规要求必须真删除(GDPR 的"被遗忘权")
  ✓ 适合:★需要恢复、需要审计、有引用关系的核心业务数据★

★ 替代方案:★状态字段 + 归档表★
  status = "active" / "archived"          # ★显式状态,不隐藏数据★
  或:删除时把行移到 xxx_archive 表
  → ★比"隐式过滤"更容易理解和维护★

软删除要处理六件事,缺一不可:模型字段、覆盖 Model.delete()(单对象)覆盖 QuerySet.delete()(批量)(否则 filter(...).delete() 会真删)、默认过滤的 Manager、保留一个不过滤的 Manager 放第一位、以及条件唯一约束。更重要的是要认清六个隐藏成本唯一约束失效(已删除的记录仍占着唯一值)、外键一致性CASCADE 对软删除不生效,因为没有真的 DELETE——文章软删了它的评论还「活着」)、查询性能(每个查询多一个条件,is_deleted 要加索引,更好的是条件索引)、数据膨胀统计口径容易出错、以及第三方包可能绕过你的 Manager。所以要想清楚:「只是怕删错」应该用备份和审计日志,软删除适合「需要恢复、需要审计、有引用关系的核心业务数据」;更容易维护的替代方案是显式的状态字段或归档表(不隐藏数据)。

五、多租户与其他实践模式

★ 模式一:多租户自动过滤(★安全相关★)
  from threading import local
  _ctx = local()                          # ★或用 contextvars(异步安全)★

  class TenantQuerySet(models.QuerySet):
      def for_current_tenant(self):
          return self.filter(tenant_id=get_current_tenant())

  class TenantManager(models.Manager.from_queryset(TenantQuerySet)):
      def get_queryset(self):
          qs = super().get_queryset()
          tid = getattr(_ctx, "tenant_id", None)
          if tid is None:
              raise RuntimeError("未设置租户上下文")   # ★fail fast,防越权★
          return qs.filter(tenant_id=tid)

  # 中间件里设置:_ctx.tenant_id = request.user.tenant_id
  ★ 注意:★异步/多线程下要用 contextvars 而不是 threading.local★
  ★ 风险:★隐式过滤一旦失效就是越权★ → 关键操作仍要显式校验

★ 模式二:常用查询封装(★最普遍的用途★)
  class OrderQuerySet(models.QuerySet):
      def pending_payment(self):
          return self.filter(status="pending",
                             created__gte=now() - timedelta(hours=2))
      def needs_shipping(self):
          return self.filter(status="paid", shipped_at__isnull=True)
  → ★业务规则集中在一处★,改规则只改这里

★ 模式三:预加载的默认值
  class ArticleManager(models.Manager.from_queryset(ArticleQuerySet)):
      def get_queryset(self):
          return super().get_queryset().select_related("author")
  ★ 好处:避免忘记 select_related 导致 N+1
  ★ 风险:★不需要 author 时也会 JOIN★ → 谨慎使用
  ✓ 更好:定义 with_author() 方法,让调用方选择

★ 模式四:Manager 专属方法(不适合放 QuerySet 的)
  class UserManager(BaseUserManager):
      def create_user(self, email, password, **kw): ...    # ★"创建"是入口级操作★
      def create_superuser(self, ...): ...
  ★ 判断标准:这个方法在"已过滤的集合"上有意义吗?
    - create_user → ★没有★(不依赖当前集合)→ Manager
    - published   → ★有★(在任何集合上都能继续过滤)→ QuerySet

★ 模式五:抽象基类 + 通用 Manager
  class TimeStampedQuerySet(models.QuerySet):
      def recent(self, days=7): ...
      def created_between(self, a, b): ...
  class TimeStampedModel(models.Model):
      created = models.DateTimeField(auto_now_add=True)
      updated = models.DateTimeField(auto_now=True)
      objects = TimeStampedQuerySet.as_manager()
      class Meta:
          abstract = True                  # ★抽象基类★
  ★ 注意:★Manager 会被子类继承★(除非子类自己定义)

★ 测试自定义 QuerySet(★很容易测★):
  def test_published():
      Article.objects.create(status="draft")
      a = Article.objects.create(status="published")
      assert list(Article.objects.published()) == [a]
  ★ 这是"把逻辑放在 QuerySet 上"的又一个好处:★可独立测试★

五种实践模式各有场景。多租户自动过滤是安全相关的——在 get_queryset() 里自动加 tenant_id 过滤,没有租户上下文时直接抛异常(fail fast)防越权;注意异步和多线程下要用 contextvars 而不是 threading.local,而且隐式过滤一旦失效就是越权,关键操作仍要显式校验。常用查询封装是最普遍的用途——把业务规则(「待支付 = pending 且 2 小时内」)集中在一处,改规则只改这里。get_queryset() 里默认 select_related 要谨慎(不需要时也会 JOIN),更好的是定义 with_author() 让调用方选择。判断一个方法该放 Manager 还是 QuerySet 有个简单标准:「这个方法在已过滤的集合上有意义吗」——create_user 没有(放 Manager),published 有(放 QuerySet)。最后,把逻辑放在 QuerySet 上的又一个好处是可以独立测试

六、实践清单

★ 设计检查清单:
  □ ★查询方法写在 QuerySet 上★(不是 Manager)
  □ 用 ★as_manager() 或 from_queryset()★ 暴露
  □ 每个方法 ★return self.filter(...)★(保持可链式)
  □ ★第一个定义的 Manager 不做任何过滤★(_base_manager)
  □ 需要默认过滤时用 ★Meta.base_manager_name★ 显式指定
  □ 定义了 Manager 后★记得保留 objects★(Django 不再自动加)
  □ 软删除:★同时覆盖 Model.delete 和 QuerySet.delete★
  □ 软删除:★条件唯一约束 + is_deleted 索引★
  □ 多租户:★用 contextvars★,且关键操作显式校验
  □ ★给 QuerySet 方法写单测★

★ 常见错误速查:
  ┌──────────────────────────────┬────────────────────────────┐
  │ 现象                          │ 原因                        │
  ├──────────────────────────────┼────────────────────────────┤
  │ objects 不存在(AttributeError)│ ★定义了别的 Manager 后没保留★│
  │ qs.myMethod() 报 AttributeError│ ★方法定义在 Manager 上了★   │
  │ 关联对象 DoesNotExist          │ ★_base_manager 带了过滤★    │
  │ filter(...).delete() 真删了     │ ★没覆盖 QuerySet.delete★    │
  │ 软删除后不能新建同名记录        │ ★唯一约束没加 condition★    │
  │ admin 里看不到某些记录          │ _default_manager 过滤了      │
  │ 级联删除漏了记录               │ ★_base_manager 过滤了★      │
  └──────────────────────────────┴────────────────────────────┘

★ 一个完整的推荐结构:
  # managers.py
  class ArticleQuerySet(models.QuerySet):
      def published(self): ...
      def by_author(self, u): ...
      def with_stats(self): ...
      def delete(self):                     # 软删除
          return self.update(is_deleted=True, deleted_at=timezone.now())

  class ArticleManager(models.Manager.from_queryset(ArticleQuerySet)):
      def get_queryset(self):
          return super().get_queryset().filter(is_deleted=False)

  # models.py
  class Article(TimeStampedModel):
      all_objects = models.Manager()        # ★第一个:不过滤★
      objects = ArticleManager()            # 业务默认
      class Meta:
          default_manager_name = "objects"
          base_manager_name = "all_objects" # ★显式★
          constraints = [UniqueConstraint(fields=["slug"],
                                          condition=Q(is_deleted=False),
                                          name="uniq_alive_slug")]

★ 一句话总结:
  ★"Manager 是入口、QuerySet 是构建器;
    把查询逻辑写在 QuerySet 上并用 from_queryset 暴露,
    就能同时获得『可读的业务方法』和『任意链式组合』;
    但要记住第一个 Manager 不能过滤——关联和级联都用它。"★

设计检查清单里最关键的五条:查询方法写在 QuerySet 上每个方法 return self.filter(...) 保持可链式第一个定义的 Manager 不做任何过滤定义了 Manager 后记得保留 objects软删除要同时覆盖两处 delete() 并配条件唯一约束。那张常见错误对照表很实用:objects 不存在是因为定义了别的 Manager 后没保留qs.myMethod() 报错是因为方法定义在了 Manager 上关联对象 DoesNotExist 和级联删除漏记录都是 _base_manager 带了过滤filter(...).delete() 真删了是因为没覆盖 QuerySet.delete()。推荐的项目结构是把 QuerySet 和 Manager 放在单独的 managers.py 里,模型里则明确写出两个 Manager 并用 Meta 显式指定 default_manager_namebase_manager_name

记忆钩子:「★Manager 是模型的数据库访问入口(objects),QuerySet 是一次查询的构建器★——Manager 本质是『QuerySet 工厂 + 一层转发』(filter/all 都转发给 get_queryset())。★关键区别是可链式★:QuerySet 的方法返回新 QuerySet 所以能一直串,而★写在 Manager 上的方法只能当起点★(Article.objects.published() 可以,但 filter(…).published() 会 AttributeError)。★所以查询逻辑一律写在 QuerySet 子类上,再用 as_manager()(最简洁)或 Manager.from_queryset()(还需要 Manager 专属方法时)暴露★——这样两端都能链式,Order.objects.for_user(u).paid().this_month() 读起来就是业务语言,且每段★可独立测试★。判断方法该放哪:★『它在已过滤的集合上有意义吗』★——create_user 没有(放 Manager)、published 有(放 QuerySet)。★两个核心概念:_default_manager 用于 admin/表单/关联管理器的基类,_base_manager 用于跟随外键、级联删除、dumpdata★——★两者默认都是『类里第一个定义的 Manager』★,所以官方建议★第一个 Manager 绝不能过滤★,否则会出现『★comment.article 抛 DoesNotExist★』『级联删除漏记录』『dumpdata 不完整』;正确做法是把不过滤的 all_objects 放第一位,或用 ★Meta.base_manager_name 显式指定★。还有个易踩点:★一旦定义了任何 Manager,Django 就不再自动添加 objects★。软删除要做六件事:字段 + ★同时覆盖 Model.delete 和 QuerySet.delete★(否则 filter().delete() 真删)+ 默认过滤的 Manager + 不过滤的第一 Manager + ★条件唯一约束 UniqueConstraint(condition=Q(is_deleted=False))★(否则删了也不能新建同名)+ is_deleted 索引;还要清楚它的隐藏成本:★CASCADE 对软删除不生效★、数据膨胀、统计口径易错、第三方包会绕过。多租户过滤要用 ★contextvars 而不是 threading.local★,且★隐式过滤失效就是越权★,关键操作仍要显式校验。」

七、常见误区与追问

  • 误区:查询方法写在 Manager 还是 QuerySet 上都一样。 差别在能不能链式调用。写在 Manager 上的方法只能作为链的起点——Article.objects.published() 可以,但 Article.objects.filter(author=u).published() 会抛 AttributeError,因为 filter() 返回的是 QuerySet 对象,它身上并没有你定义在 Manager 上的方法。而把方法写在 QuerySet 子类上、再用 as_manager()from_queryset() 暴露,就能同时获得两种用法:既可以 Article.objects.published()(Manager 起点),也可以 Article.objects.filter(x=1).published().recent()(任意串联)。所以规则很明确:除非这个方法「不依赖当前集合」(如 create_user),否则一律写在 QuerySet 上
  • 误区:为了实现软删除,把带过滤的 Manager 定义成第一个(或唯一一个)就行。 这会破坏 Django 内部依赖 _base_manager 的一系列机制。_base_manager 默认就是类里第一个定义的 Manager,Django 在这些场景用它:跟随外键加载关联对象comment.article)、级联删除dumpdata 序列化。如果它带了 filter(is_deleted=False),后果是:读一条评论时它关联的文章「凭空消失」并抛 DoesNotExist(明明只是想读关联,却被过滤挡住)、删除父对象时被软删除的子记录不被处理、导出的数据不完整、admin 的关联下拉框缺项。Django 官方文档明确要求 _base_manager 不要过滤任何行。正确做法是把不过滤的 Manager 放在第一位all_objects = models.Manager()),再用 Meta.default_manager_name 指定业务默认用哪个,或直接用 Meta.base_manager_name 显式声明。
  • 误区:定义了自定义 Manager 之后,Model.objects 仍然可用。 一旦你在模型里定义了任何 Manager,Django 就不再自动添加 objects。所以 class Article(models.Model): published = PublishedManager() 之后,Article.objects 会抛 AttributeError——而项目里大量代码(包括第三方包、admin、DRF)都默认使用 objects,这会引发一连串难以定位的报错。正确做法是显式保留objects = models.Manager()objects = ArticleQuerySet.as_manager(),然后再加其他命名 Manager。顺带一提,Manager 会被子类继承(除非子类自己定义了同名的),所以在抽象基类里定义通用 Manager 是一个很好的复用手段。
  • 误区:软删除只要覆盖 Model.delete() 就完整了。 漏掉了批量删除路径Article.objects.filter(status="draft").delete() 调用的是 QuerySet.delete(),它不会逐个调用模型的 delete() 方法(出于性能考虑,它直接发一条 SQL DELETE)——所以数据会被真正删除。必须同时覆盖 QuerySet.delete()(改成 self.update(is_deleted=True, ...))。同类的「绕过路径」还有好几条:bulk_create 不触发 save()queryset.update() 不触发信号也不更新 auto_now级联删除(on_delete=CASCADE)走的是数据库或 Django 的删除收集器,不会调用你的软删除逻辑。此外还要记得唯一约束——已软删除的记录仍占着唯一值,必须改用 UniqueConstraint(condition=Q(is_deleted=False)),否则「删除后无法用同样的 slug 重新创建」。
  • 误区:在 get_queryset() 里加 select_related 能一劳永逸地避免 N+1。 它确实能避免忘记预加载,但代价是每一次查询都会 JOIN——包括那些根本不需要关联数据的场景(比如只统计 count()、只取 values_list("id")、或者只更新一个字段)。表变宽、JOIN 变多之后,这个「默认优化」会变成默认的性能负担,而且很难被发现(因为它藏在 Manager 里,调用方看不到)。更好的做法是提供一个显式的 QuerySet 方法def with_author(self): return self.select_related("author"),让调用方按需选择——既有复用又保留了控制权。同样的道理适用于在 get_queryset() 里加 order_by(会覆盖调用方的排序意图)和 annotate(无谓地增加聚合开销)。
  • 追问:_default_manager_base_manager 到底分别用在哪些地方? _default_manager(默认是第一个定义的 Manager,可用 Meta.default_manager_name 指定)用于「面向用户的默认入口」:Django admin 的列表页、ModelFormModelChoiceField 下拉选项、DRF 的默认 queryset、序列化、以及关联管理器的基类article.comments 这个 RelatedManager 是基于 Comment._default_manager 的类创建的,所以 Comment 的默认过滤在这里会生效)。_base_manager(默认也是第一个,可用 Meta.base_manager_name 指定)用于「Django 内部需要拿到完整数据的场合」:跟随外键加载单个关联对象comment.article)、删除时收集级联对象dumpdata。设计上的建议是:让第一个 Manager 保持「不过滤、最完整」,把带过滤的业务 Manager 通过 default_manager_name 指定——这样既让日常查询自动过滤,又不破坏 Django 的内部机制。
  • 追问:as_manager()from_queryset() 该怎么选? MyQuerySet.as_manager() 是最简洁的写法,Django 会自动生成一个 Manager 并把 QuerySet 的所有公开方法(不以下划线开头、且没标 queryset_only = True)复制过去——适合「只需要把 QuerySet 方法暴露出来」的场景,绝大多数情况用它就够了。Manager.from_queryset(MyQuerySet) 返回的是一个 Manager 类(注意要再实例化:objects = ArticleManager()),你可以继承它并添加 Manager 专属的东西——两种典型需求:① 覆盖 get_queryset()(做默认过滤、默认预加载);② 添加不适合放在 QuerySet 上的方法create_user() 这类「创建」操作,它不依赖当前的过滤集合)。补充一个细节:如果某个 QuerySet 方法不应该出现在 Manager 上(比如 delete()update() 这类「只应作用于已过滤集合」的操作),可以给它设 方法.queryset_only = True
  • 追问:软删除和「归档表」方案该怎么权衡? 软删除(加 is_deleted 标记 + 默认过滤)的优点是实现简单、恢复容易、外键关系保持完整(引用不会断),缺点是:唯一约束需要改成条件唯一每个查询都多一个条件(要加索引,最好是条件索引)、数据无限膨胀(大表上删除的行永远占空间、拖慢全表扫描和备份)、CASCADE 不生效(父对象软删了子对象还「活着」,一致性要自己维护)、以及统计口径容易出错(第三方包和直接写 SQL 的地方会绕过你的 Manager)。归档表(删除时把行移到 xxx_archive 表)的优点是主表保持精简、约束和索引不受影响、语义显式,缺点是外键会断(归档表通常不带约束)、恢复要写迁移逻辑跨表查询麻烦。选择建议:核心业务实体且需要频繁恢复/审计 → 软删除日志类、流水类、数据量巨大且几乎不恢复 → 归档表或分区表只是「怕删错」→ 其实应该用数据库备份 + 审计日志,而不是给所有查询增加负担

八、加强记忆

Manager 是模型的数据库访问入口(objects),QuerySet 是一次查询的构建器——Manager 本质是「QuerySet 工厂 + 一层转发」(filter/all 都转发给 get_queryset())。关键区别是可链式:QuerySet 的方法返回新 QuerySet 所以能一直串下去,而写在 Manager 上的方法只能当起点Article.objects.published() 可以,但 filter(...).published()AttributeError)。所以查询逻辑一律写在 QuerySet 子类上,再用 as_manager()(最简洁)或 Manager.from_queryset()(还需要 Manager 专属方法或要覆盖 get_queryset() 时)暴露——这样两端都能链式,Order.objects.for_user(u).paid().this_month() 读起来就是业务语言,而且每段逻辑可独立测试。判断方法该放哪有个简单标准:「它在已过滤的集合上有意义吗」——create_user 没有(放 Manager)、published 有(放 QuerySet)。两个核心概念_default_manager 用于 admin/表单/关联管理器的基类,_base_manager 用于跟随外键、级联删除、dumpdata——两者默认都是「类里第一个定义的 Manager」,所以官方建议第一个 Manager 绝不能过滤,否则会出现「comment.articleDoesNotExist」「级联删除漏记录」「dumpdata 不完整」;正确做法是把不过滤的 all_objects 放第一位,或用 Meta.base_manager_name 显式指定。还有个易踩点:一旦定义了任何 Manager,Django 就不再自动添加 objects。软删除要做六件事:加字段 + 同时覆盖 Model.delete()QuerySet.delete()(否则 filter().delete() 会真删)+ 默认过滤的 Manager + 不过滤的第一 Manager + 条件唯一约束 UniqueConstraint(condition=Q(is_deleted=False))(否则删了也无法新建同名)+ is_deleted 索引;同时要清楚它的隐藏成本:CASCADE 对软删除不生效、数据膨胀、统计口径易错、第三方包会绕过。多租户自动过滤要用 contextvars 而不是 threading.local,而且隐式过滤一旦失效就是越权,关键操作仍要显式校验。