Filter concepts by levelShowing all levels.

Django · Section 34

Django Managers

Level
advanced
Read
20 min
Concepts
2

A custom manager subclasses models.Manager to add reusable, table-level query methods, or overrides get_queryset() to change the default starting point for every query through that manager — a model can have multiple, independently-named managers at once. Django tracks two special roles separately: the DEFAULT manager (used by admin/dumpdata/migrations, safely filterable) and the BASE manager (used when following a relationship to a related object, which must almost always stay unfiltered, or relationship traversal silently breaks for excluded rows). The roadmap's own explicit question — when does manager logic become an inappropriate business-logic container — mirrors the model-methods distinction one layer up: a manager should hold pure query logic, and the moment a manager method sends emails, calls external services, or holds rules unrelated to filtering, that logic belongs in a service function instead.

This section

What is true here

  1. A manager method is a named, reusable, opt-in query shortcut; overriding get_queryset() changes the DEFAULT starting point for every query through that specific manager, unconditionally.
  2. A model can declare multiple independently-named managers, each with its own optional get_queryset() override.
  3. The default manager (used by admin/dumpdata/migrations) can safely be filtered; the base manager (used for relationship traversal) must almost always stay unfiltered, or related-object lookups silently break for excluded rows.
  4. Row-level logic (operating on one instance) belongs on the model itself as an instance method, never on a manager, which operates at the table level.
  5. A manager should hold pure query logic — the moment a manager method produces side effects unrelated to querying, it has outgrown being a manager and belongs in a service function instead.

What you will be able to do

  • Write a custom manager with reusable query methods and/or an overridden get_queryset()
  • Attach multiple managers to one model correctly
  • Set base_manager_name correctly, avoiding the filtered-base-manager anti-pattern
  • Recognize when manager logic has outgrown a manager and belongs in a service layer

Custom managers and get_queryset()

Adding reusable query methods, changing the default starting queryset, and attaching multiple managers to one model.

Custom managers and get_queryset()

coreintermediate

A custom manager subclasses models.Manager and adds reusable, TABLE-LEVEL query methods — Order.objects.completed() instead of repeating Order.objects.filter(status="COMPLETED") everywhere it's needed. Overriding get_queryset() changes the STARTING point every query on that manager begins from (e.g. always excluding soft-deleted rows). A model can have MULTIPLE managers (Book.objects, Book.dahl_objects) — each is a completely independent named entry point with its own get_queryset().

Think of it as

A manager is the object sitting at Model.objects — every query starts by asking a manager for a QuerySet. A custom manager method (like with_counts()) is a shortcut for a query shape used often enough to deserve a name, exactly like a well-named function replaces a repeated expression. Overriding get_queryset() is a stronger move — it changes the DEFAULT starting QuerySet for that manager, which is why Book.objects.all() and Book.dahl_objects.all() can return genuinely different sets of Book rows from the exact same table, depending only on which manager was used to reach them. Multiple managers on one model exist because sometimes you want SEVERAL distinct "views" into the same table available at once — one manager per meaningful default filter, each independently named and independently usable.

python
class MyManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(...)   # changes the default starting point

    def a_shortcut(self):
        return self.filter(...)   # a named, opt-in query

What we're doing: Give a Book model both an unfiltered default manager and a second, filtered manager, so the same table can be queried two different ways depending on which manager is used.

library/models.pypython
class DahlBookManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(author="Roald Dahl")

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.CharField(max_length=50)

    objects = models.Manager()       # unfiltered — every book
    dahl_objects = DahlBookManager()  # filtered — only Roald Dahl's books
2
get_queryset() is overridden here, not a new method added — this means EVERY query through dahl_objects (not just a specific method call) starts pre-filtered.
8
Book.objects.all() returns every book — this manager's get_queryset() was never overridden, so it stays the plain default.

Why this works: Book.dahl_objects.filter(title__startswith="The") still only ever returns Roald Dahl books, since the filter is applied ON TOP of get_queryset()'s already-narrowed starting point — this is what makes an overridden get_queryset() different from a manager method: it applies unconditionally to every query through that manager, not just one named shortcut.

Putting row-level (single-instance) logic on a manager instead of the model itself

Wrong

python
class PersonManager(models.Manager):
    def mark_deleted(self):   # operates on ONE instance — wrong place
        self.deleted = True
        self.save()

Better

python
class Person(models.Model):
    def mark_deleted(self):   # a model instance method — the right place
        self.deleted = True
        self.save()

What you see: The manager method as written does not even make sense to call — self inside a Manager method refers to the MANAGER, not to any particular Person row, so there is no single instance for self.deleted = True to apply to.

Why: A manager operates at the TABLE level — its methods return QuerySets or aggregate results spanning potentially many rows, and have no natural concept of "the current row." A method that only makes sense for ONE specific instance (marking IT as deleted) belongs on the model class itself, as an ordinary instance method, exactly the "persistence/business behavior belongs on the model, orchestration doesn't" distinction from earlier in this topic, applied to the manager-vs-model boundary specifically.

Two managers, two independent starting points, same table

Book table

every row

objects

plain Manager — unfiltered

dahl_objects

get_queryset() filters author="Roald Dahl"

.all() → every book

.all() → only Dahl books

  • Book table — every row
    • leads to objects
    • leads to dahl_objects
  • objects — plain Manager — unfiltered
    • leads to .all() → every book
  • dahl_objects — get_queryset() filters author="Roald Dahl"
    • leads to .all() → only Dahl books
  • .all() → every book
  • .all() → only Dahl books

The two ways to customize a manager

The two ways to customize a manager
ApproachEffect
Add a method (e.g. with_counts())a named, reusable query shortcut — called explicitly, opt-in
Override get_queryset()changes the DEFAULT starting point for every query through that manager — always applies

Together

python
class OrderManager(models.Manager):
    def completed(self):
        return self.filter(status="COMPLETED")

class Order(models.Model):
    status = models.CharField(max_length=20)
    objects = OrderManager()

Order.objects.completed()   # a named shortcut, called explicitly

Remember: A manager method is a named, reusable, opt-in query shortcut; overriding get_queryset() changes the DEFAULT starting point for every query through that specific manager. A model can have multiple independently-named managers. Row-level logic (operating on one instance) belongs on the model itself, never on a manager.

See also: default vs base manager · custom queryset classes · instance methods and domain logic

Advertisement

Default vs base manager, and manager vs business logic

The two special manager roles Django tracks separately, and where manager logic should stop.

Default vs base manager, and manager vs business logic

coreadvanced

The DEFAULT manager (the first one declared, or Meta.default_manager_name) is what most Django-internal operations use (admin, dumpdata, migrations). The BASE manager (Meta.base_manager_name, defaulting to the plain, unfiltered Manager) is specifically what Django uses when following a RELATIONSHIP to a related object — it should almost never filter anything, because Django needs to be able to find a related object even if a "normal" filtered manager would have excluded it. A manager should stay a query interface — the moment a manager method starts sending emails, calling external services, or holding business rules unrelated to filtering/querying, that logic has outgrown the manager and belongs in a service layer instead.

Think of it as

Two entirely different managers can both plausibly be called "the important one," which is exactly why Django tracks them separately. The DEFAULT manager is about ergonomics — which manager's queryset shows up for admin list views, dumpdata exports, and similar Django-internal conveniences; it can safely be a filtered manager if that filtering is genuinely the sensible default view. The BASE manager is about CORRECTNESS — when Django needs to look up a related object (e.g. following a ForeignKey backward), it uses the base manager specifically because it must be able to find that object REGARDLESS of any custom filtering a "normal" manager might apply, or relationship traversal would silently break for rows a filtered manager happens to exclude. Setting base_manager_name to a filtered manager is a real, documented mistake for exactly this reason. Separately, the roadmap's own explicit question — when does manager logic become inappropriate — is really the same "persistence vs business vs orchestration" distinction as model methods, applied one layer up: a manager should hold QUERY logic, and the moment it starts holding logic unrelated to querying (sending notifications, calling a payment API, enforcing a business rule that has nothing to do with filtering rows), that has become a business-logic container wearing a manager's clothes.

python
class Meta:
    base_manager_name = "all_objects"   # must stay unfiltered
    default_manager_name = "objects"    # can be filtered, if that's the sensible default

What we're doing: Correctly set base_manager_name to an explicitly unfiltered manager, so relationship traversal can always find related objects regardless of a default manager's filtering.

library/models.pypython
class Book(models.Model):
    class Meta:
        base_manager_name = "all_objects"

    all_objects = models.Manager()             # unfiltered — used for relationship traversal
    objects = ActiveBookManager()               # filtered (e.g. excludes archived books) — the default
2
base_manager_name points at all_objects, the UNFILTERED manager — this is what Django uses internally when following a relationship TO a Book, ensuring even an archived book (excluded by the default objects manager) can still be found via a relationship.
6
objects (the default) stays filtered — fine, since it's only used for direct queries and Django-internal conveniences, not relationship traversal.

Why this works: If base_manager_name were left pointing at the filtered objects manager instead, a ForeignKey relationship to an ARCHIVED book would silently fail to resolve — Django would look for the related Book using a manager that has already excluded it, exactly the documented anti-pattern this concept warns against.

Turning a manager method into a business-logic container that sends emails or calls external services

Wrong

python
class OrderManager(models.Manager):
    def mark_all_pending_as_expired(self):
        expired = self.filter(status="PENDING", created_at__lt=cutoff)
        for order in expired:
            send_expiration_email(order)   # a manager sending emails — outgrown its role
        expired.update(status="EXPIRED")

Better

python
class OrderManager(models.Manager):
    def pending_before(self, cutoff):
        return self.filter(status="PENDING", created_at__lt=cutoff)   # pure query, stays a manager's job

# a service function, not the manager, handles the side effects:
def expire_stale_orders(cutoff):
    expired = Order.objects.pending_before(cutoff)
    for order in expired:
        send_expiration_email(order)
    expired.update(status="EXPIRED")

What you see: Every test exercising ANY query through OrderManager now potentially needs a mocked email backend, and a completely unrelated data migration or admin action that happens to call this manager method triggers a wave of emails nobody expected from what looked like a simple query operation.

Why: A manager's entire purpose is to be the QUERY interface for a model — the moment a manager method starts producing side effects (sending emails, calling external APIs) alongside its query, every caller of that method inherits those side effects whether they want them or not, exactly the same "orchestration doesn't belong on the model" lesson from model methods, applied one layer up to managers instead. Splitting the pure query (pending_before()) from the orchestration (expire_stale_orders()) keeps the manager safely reusable everywhere a plain query would be expected.

Default manager vs base manager

Default manager

  • +Used by admin, dumpdata, migrations
  • +Safe to filter, if that's the sensible view
  • +Set via Meta.default_manager_name

Base manager

  • Used when Django follows a relationship
  • Almost never filter — must find every related row
  • Set via Meta.base_manager_name
  • Default manager
    • Used by admin, dumpdata, migrations
    • Safe to filter, if that's the sensible view
    • Set via Meta.default_manager_name
  • Base manager
    • Used when Django follows a relationship
    • Almost never filter — must find every related row
    • Set via Meta.base_manager_name

Default manager vs base manager

Default manager vs base manager
AspectDefault managerBase manager
Set viaMeta.default_manager_name (or first declared)Meta.base_manager_name
Used byadmin, dumpdata, migrations, most Django-internal opsfollowing a relationship to a related object
Safe to filter?yes, if that's the sensible default viewalmost never — must find every related object

Together

python
class Book(models.Model):
    class Meta:
        base_manager_name = "all_objects"   # explicitly unfiltered — correct

    all_objects = models.Manager()          # base manager — no filtering
    dahl_objects = DahlBookManager()        # a filtered, opt-in manager — fine as a NAMED manager

Remember: The default manager (Django-internal conveniences like admin/dumpdata) can safely be filtered; the base manager (relationship traversal) must almost always stay unfiltered, or related-object lookups silently break for excluded rows. A manager should hold pure query logic — the moment a manager method sends emails, calls external services, or holds business rules unrelated to filtering, that logic belongs in a service function instead.

See also: custom managers and get queryset · instance methods and domain logic · manager and queryset pairing

Advertisement