Skip to content

Managers

Managers are a powerful feature in Edgy, heavily inspired by Django's manager system. They allow you to create tailored, reusable query sets for your models. Unlike Django, Edgy managers are instance and class aware. For every inheritance, they are shallow copied, and if used on an instance, you also have a shallow copy that you can customize.

Note: Shallow copy means that deeply nested or mutable attributes must be copied, not modified. Alternatively, you can override __copy__ to handle this for you.

Edgy uses the query manager by default for direct queries, which simplifies understanding. For related queries, query_related is used, which is a RedirectManager by default that redirects to query.

Let's look at a simple example:

import edgy
from edgy import Database, Registry

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


class User(edgy.Model):
    name: str = edgy.CharField(max_length=255)
    email: str = edgy.EmailField(max_length=70)
    is_active: bool = edgy.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="Edgy", email="foo@bar.com")  # noqa

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

When querying the User table, the query manager is the default and should always be present.

Inheritance

Managers can set the inherit flag to False to prevent subclasses from using them. This is similar to how fields work. This is useful for injected managers, though we don't have any yet.

Custom Managers

You can create your own custom managers by inheriting from the Manager class and overriding the get_queryset() method. For more extensive customization, you can use the BaseManager class, which is more extensible.

For those familiar with Django managers, the concept is exactly the same. 😀

Managers must be type annotated as ClassVar, or an ImproperlyConfigured exception will be raised.

from typing import ClassVar

import edgy
from edgy import Manager, QuerySet


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(edgy.Model):
    # Add the new manager
    inactives: ClassVar[Manager] = InactiveManager()

Let's create a new manager and use it with our previous example:

from typing import ClassVar

import edgy
from edgy 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(edgy.Model):
    name: str = edgy.CharField(max_length=255)
    email: str = edgy.EmailField(max_length=70)
    is_active: bool = edgy.BooleanField(default=True)

    # Add the new manager
    inactives: ClassVar[Manager] = 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="Edgy", email="foo@bar.com", is_active=False)  # noqa

# You can also create a user using the new manager
await User.inactives.create(name="Another Edgy", 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="Edgy", email="user@edgy.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)]

These managers can be as complex as you like, with as many filters as you need. Simply override get_queryset() and add the manager to your models.

Overriding the Default Manager

You can override the default manager by creating a custom manager and overriding the query manager. By default, query is also used for related queries. This can be customized by setting an explicit query_related manager.

from typing import ClassVar

import edgy
from edgy 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(edgy.Model):
    name: str = edgy.CharField(max_length=255)
    email: str = edgy.EmailField(max_length=70)
    is_active: bool = edgy.BooleanField(default=True)

    # Add the new manager
    query: ClassVar[Manager] = 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="Edgy", email="foo@bar.com", is_active=False)  # noqa

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

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

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

Now, let's override only the related manager:

from typing import ClassVar

import edgy
from edgy 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 Team(edgy.Model):
    name = edgy.CharField(max_length=100)

    class Meta:
        registry = models


class User(edgy.Model):
    name: str = edgy.CharField(max_length=255)
    email: str = edgy.EmailField(max_length=70)
    team = edgy.ForeignKey(Team, null=True, related_name="members")
    is_active: bool = edgy.BooleanField(default=True)

    # Add the new manager only for related queries
    query_related: ClassVar[Manager] = 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 a Team using the default manager
team = await Team.query.create(name="Edgy team")  # noqa

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

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

# Create a user using the default manager
user3 = await User.query.create(name="Edgy", email="user@edgy.com", team=team)  # noqa


# Querying them all
users = await User.query.all()  # noqa
assert [user1, user2, user3] == users
# now with team
users2 = await team.members.all()  # noqa
assert [user1, user2] == users2

Warning

Be careful when overriding the default manager, as you might not get all the results from .all() if you don't filter properly.

Key Concepts and Benefits

  • Reusability: Managers allow you to encapsulate complex query logic and reuse it across your application.
  • Organization: They help keep your model definitions clean and organized by moving query logic out of the model class.
  • Customization: You can create managers that are tailored to the specific needs of your models.
  • Instance and Class Awareness: Edgy managers are aware of the instance and class they are associated with, allowing for more dynamic and context-aware queries.
  • Inheritance Control: The inherit flag allows you to control whether managers are inherited by subclasses.
  • Separation of Concerns: Managers allow you to separate query logic from model definitions, leading to cleaner and more maintainable code.

Use Cases

  • Filtering by Status: Create a manager that only returns active records.
  • Ordering by Specific Fields: Create a manager that returns records ordered by a specific field or set of fields.
  • Aggregations: Create a manager that performs aggregations on your data, such as calculating averages or sums.
  • Complex Joins: Create a manager that performs complex joins between multiple tables.
  • Custom Query Logic: Create a manager that implements custom query logic that is specific to your application.

By using managers effectively, you can create more powerful and maintainable Edgy applications.