Django 的 Manager 和 QuerySet 有什么区别?怎么自定义?
简化版
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 对比:
| Manager | QuerySet | |
|---|---|---|
| 是什么 | 模型的数据库访问入口(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.article 抛 DoesNotExist)、级联删除漏记录、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_name 和 base_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 的列表页、ModelForm的ModelChoiceField下拉选项、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.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,而且隐式过滤一旦失效就是越权,关键操作仍要显式校验。