Skip to content

Managers

Managers are the bridge between model classes and querysets.

If a model is the description of a table, a manager is the entry point that decides which queryset object to hand back for that model and in which context that queryset should run.

Default behavior

Every concrete model gets a default query manager.

Managers in Saffier are descriptors, so they behave differently depending on where you access them:

  • on the model class, they are class-bound
  • on a model instance, they are shallow-copied and instance-bound

That instance binding matters when schema selection or database selection comes from the current model instance.

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models
        unique_together = ["name", "email"]


# Using ipython that supports await
# Don't use this in production! Use Alembic or any tool to manage
# The migrations for you
await models.create_all()  # noqa

await User.query.create(name="Saffier", email="foo@bar.com")  # noqa

user = await User.query.get(id=1)  # noqa
# User(id=1)

When to create a custom manager

Create a custom manager when you want:

  • a reusable default filter such as “only active rows”
  • project-specific query helpers kept close to the model
  • a specialized queryset class for one family of models

The usual pattern is to subclass saffier.Manager and override get_queryset().

import saffier
from saffier import Database, Manager, QuerySet, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class InactiveManager(Manager):
    """
    Custom manager that will return only active users
    """

    def get_queryset(self) -> "QuerySet":
        queryset = super().get_queryset().filter(is_active=False)
        return queryset


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70)
    is_active = saffier.BooleanField(default=True)

    # Add the new manager
    inactives = InactiveManager()

    class Meta:
        registry = models
        unique_together = ["name", "email"]


# Using ipython that supports await
# Don't use this in production! Use Alembic or any tool to manage
# The migrations for you
await models.create_all()  # noqa

# Create an inactive user
await User.query.create(name="Saffier", email="foo@bar.com", is_active=False)  # noqa

# You can also create a user using the new manager
await User.inactives.create(name="Another Saffier", email="bar@foo.com", is_active=False)  # noqa

# Querying using the new manager
user = await User.inactives.get(email="foo@bar.com")  # noqa
# User(id=1)

user = await User.inactives.get(email="bar@foo.com")  # noqa
# User(id=2)

# Create a user using the default manager
await User.query.create(name="Saffier", email="user@saffier.com")  # noqa

# Querying all inactives only
users = await User.inactives.all()  # noqa
# [User(id=1), User(id=2)]

# Querying them all
user = await User.query.all()  # noqa
# [User(id=1), User(id=2), User(id=3)]

In real projects this is useful for things like:

  • hiding soft-deleted rows by default
  • adding tenant scoping rules
  • exposing common helper methods such as published() or for_account()

Practical pattern: multiple managers on the same model

It is often better to keep query unfiltered and add an extra filtered manager instead of replacing the default manager immediately.

class User(saffier.Model):
    query: ClassVar[saffier.Manager] = saffier.Manager()
    active: ClassVar[ActiveUsersManager] = ActiveUsersManager()

That gives you both:

  • await User.query.all() for the full table
  • await User.active.all() for the opinionated subset

Overriding the default manager

Overriding query is supported, but it changes the semantics of every normal query entry point for that model.

import saffier
from saffier import Database, Manager, QuerySet, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class InactiveManager(Manager):
    """
    Custom manager that will return only active users
    """

    def get_queryset(self) -> "QuerySet":
        queryset = super().get_queryset().filter(is_active=False)
        return queryset


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70)
    is_active = saffier.BooleanField(default=True)

    # Add the new manager
    query = InactiveManager()

    class Meta:
        registry = models
        unique_together = ["name", "email"]


# Using ipython that supports await
# Don't use this in production! Use Alembic or any tool to manage
# The migrations for you
await models.create_all()  # noqa

# Create an inactive user
await User.query.create(name="Saffier", email="foo@bar.com", is_active=False)  # noqa

# You can also create a user using the new manager
await User.query.create(name="Another Saffier", email="bar@foo.com", is_active=False)  # noqa

# Create a user using the default manager
await User.query.create(name="Saffier", email="user@saffier.com")  # noqa

# Querying them all
user = await User.query.all()  # noqa
# [User(id=1), User(id=2)]

Warning

If you override query with a filtered manager, all(), get(), count(), and related helpers all inherit that filter. Keep an unfiltered manager available somewhere if your application still needs raw access.