Skillversitas

Managers and Custom QuerySets

By default every document gets objects, a generic manager that hands out plain querysets. When a model has a recurring set of queries - "active users", "published posts", "orders in a state" - the pattern that keeps them DRY and enforceable is a custom queryset with named methods plus a manager that returns it. The result: User.objects.active() reads like a domain concept, every call site uses the same definition, and the definition lives next to the model where it belongs.

The Pattern

from mongoengine.queryset.manager import Manager
from mongoengine.queryset.queryset import QuerySet

class UserQuerySet(QuerySet):
    def active(self):
        return self.filter(active=True, deleted_at=None)

    def with_role(self, role):
        return self.filter(roles__in=[role])

class UserManager(Manager):
    queryset_class = UserQuerySet

class User(Document):
    username = StringField(required=True, unique=True)
    active = BooleanField(default=True)
    roles = ListField(StringField(), default=list)

    objects = UserManager()

Two pieces, one contract. The custom queryset subclass adds domain methods that return self.filter(...) - still lazy, still chainable, so User.objects.active().order_by("-created_at")[:25] composes exactly like any queryset. The manager subclass points queryset_class at it, and assigning objects = UserManager() on the model wires it up. Everything else - filter, first, count - comes from the base queryset unchanged.

Why It Matters

# without: the definition is copy-pasted, and drifts
User.objects(active=True, deleted_at=None).count()

# with: one definition, one spelling, everywhere
User.objects.active().count()
User.objects.active().with_role("editor")   # methods compose
user = User.objects.active().get(username="ada")

The value is enforcement, not brevity: when "active" means something specific (a flag plus a soft-delete check), a named method makes it impossible for a call site to silently use only half the definition. It is also the hook for behavior that must always apply - a queryset method that adds .no_cache(), or a projection via .only("username", "email"), applied consistently instead of by memory.

Manager-Level Methods

class OrderManager(Manager):
    queryset_class = OrderQuerySet

    def open(self):
        # a manager method returns a queryset for further chaining
        return self.get_queryset().filter(status__in=["new", "paid"])

    def total_revenue(self, window):
        # a manager method may also be a terminal computation
        return (self.open()
                .filter(created_at__gte=window)
                .sum("total"))

class Order(Document):
    status = StringField()
    total = DecimalField()
    objects = OrderManager()

Manager methods are the place for operations that do not belong on the generic queryset: open() returns a scoped queryset (and keeps chaining), while total_revenue() is a terminal that returns a number. The split is the same as in any ORM - queryset methods build and refine querysets, manager methods package the model's canonical operations.

Gotchas

  • The manager is per-class. Assigning objects = UserManager() replaces that class's default manager; other document classes are untouched.
  • Methods return new querysets, they do not mutate. User.objects.active() does not change what User.objects is - every method call produces a fresh lazy queryset, the same rule as filter.
  • Keep them thin. A queryset method that does business logic (sends an email, mutates state) is a smell - those belong in services; the queryset's job is to define and refine queries.
  • Document the semantics. active() means whatever its body says; a one-line docstring on each method is the contract between the model author and the two hundred call sites.

Summary

Build domain query methods on a QuerySet subclass (active(), with_role() - each returning self.filter(...)), point a Manager subclass's queryset_class at it, and assign it as the model's objects. Manager methods package terminal operations the model always needs. The payoff is a single, composable, enforceable definition of the queries your application runs most - readable at every call site, and changeable in exactly one place.