Model managers

As you've learned throughout the examples presented in this and the previous chapter, a Django model's objects reference or default model manager, offers an extensive array of functionalities to execute database operations.

Under most circumstances, Django models don't require any modifications to their default model manager or objects reference. However, there can be circumstances where the need arises to customize a Django model's default model manager or inclusively create multiple model managers.

Custom and multiple model managers

One of the main reasons to create custom Django model managers is to add custom manager methods, to make the execution of recurring queries on a model easier.

For example, running queries such as Item.objects.filter(menu__name='Sandwiches') or Item.objects.filter(menu__name='Salads') is simple, but if you start writing these same queries over and over, the process can become tiresome and error prone. This is particularly true for raw SQL queries, which take more time to write and have a higher degree of complexity.

A custom manager method allows you to write a query once as part of a model, and later invoke the custom manager method -- just like other model manager methods (e.g. all(), filter(), exclude()) -- to trigger the query. Listing 8-61 illustrates a custom model manager class with a custom method, including a model that uses it, in addition to various model manager calls.

Listing 8-61. Django custom model manager with custom manager methods

from django.db import models

# Create custom model manager
class ItemMenuManager(models.Manager):
    def salad_items(self):
        return self.filter(menu__name='Salads')
                       
    def sandwich_items(self):
        return self.filter(menu__name='Sandwiches')    

# Option 1) Override default model manager 
class Item(models.Model):
    menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
    name = models.CharField(max_length=30)
    ...
    objects = ItemMenuManager()

# Queries on default custom model manager
Item.objects.all()
Item.objects.salad_items()
Item.objects.sandwich_items()

# Option 2) Create new model manager field and leave default model manager as is
    menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
    name = models.CharField(max_length=30)
    ...
    objects = models.Manager()
    menumgr = ItemMenuManager()
    
# Queries on default and custom model managers
Item.objects.all()
Item.menumgr.salad_items()
Item.menumgr.sandwich_items()
# ERROR Item.objects.salad_items()
# 'Manager' object has no attribute 'salad_items' # ERROR Item.objects.sandwich_items()
# 'Manager' object has no attribute 'sandwich_items' Item.menumgr.all()

The first class in listing 8-61 is the ItemMenuManager that functions as a custom model manager. Notice how this class inherits its behavior from the models.Manager class, which is what gives it model manager behavior. Next, the ItemMenuManager class declares two methods that return QuerySet results. Notice how the class methods reference self -- representing the model class instance -- and call standard model methods to trigger database queries.

It's worth mentioning custom model managers don't necessarily need to use native model queries or return QuerySet data structures, custom model managers can equally contain any logic (e.g. Python DB API calls) or return any data structure (e.g. Python tuples).

Once you have a custom model manager there are two options to assign it to a model class. The first option illustrated in listing 8-61, consists of overriding a model's default model manager objects and explicitly assigning it a custom model manager. Once this is done, you can use the same objects reference to call the custom model manager methods. In addition, notice in listing 8-61 that even when overriding the default model manager objects, a model continues to have access to the built-in model manager methods (e.g. all()) because the custom model inherits its behavior from the parent models.Manager class.

Next, listing 8-61 illustrates the second option to integrate a custom model manager. This option consists of adding a new model field to reference a custom manager and leave the default manager objects as is. In this case, the custom model manager methods become accessible through the new field reference (e.g. Item.menumgr.salad_items()) and the objects reference continues to work with its default behavior.

Tip When you declare multiple model manager in a model, you can set the default model manager using the default_manager_name meta option. See the previous chapter for additional details on model meta options.
Warning If you don't define a default model manager in a multi-manager model, Django choose the first manager declared in the model. This can have unexpected behaviors in model operations that can't explicitly chose model managers (e.g. dumpdata) unlike queries that can use dot-notation to choose a model manager.

Custom model managers and QuerySet classes with methods

Model managers are closely tied to methods that return QuerySet data structures. As you've seen, nearly all methods chained to the default model manager objects (e.g. all(), filter()) generate QuerySet data structures. When you create custom model managers, it's possible to override the default behavior for these QuerySet methods, as well as create your own custom QuerySet classes and methods.

One of the most important QuerySet methods in model managers is the get_queryset() method, used to define a model's initial QuerySet or what's returned by a model manager's all() method. In custom model managers, the get_queryset() method is particularly important because it let's you filter the initial QuerySet depending on the purpose of a model manager.

Listing 8-62 illustrates multiple custom model managers that define custom logic for the get_queryset() method.

Listing 8-62. Django custom model managers with custom get_queryset() method

class SanDiegoStoreManager(models.Manager):
    def get_queryset(self):
        return super(SanDiegoStoreManager, self).get_queryset().filter(city='San Diego')

class LosAngelesStoreManager(models.Manager):
    def get_queryset(self):
        return super(LosAngelesStoreManager, self).get_queryset().filter(city='Los Angeles')    

class Store(models.Model):
    name = models.CharField(max_length=30)    
    ...
    objects = models.Manager()
    sandiego = SanDiegoStoreManager()
    losangeles = LosAngelesStoreManager()

# Call default manager all() query, backed by get_queryset() method
Store.objects.all()

# Call sandiego manager all(), backed by get_queryset() method
Store.sandiego.all()

# Call losangeles manager all(), backed by get_queryset() method
Store.losangeles.all()

The first two classes in listing 8-62 represent custom model managers, however, notice that unlike the custom model manager in listing 8-61, both the SanDiegoStoreManager and LosAngelesStoreManager classes define the get_queryset() method. In both cases, the get_queryset() method returns a QuerySet generated by calling the parent model manager get_queryset() method (i.e. all()) -- via the super() method -- and applying an additional filter() to the parent depending on the purpose of the custom model manager (e.g. get stores by city).

Once the custom managers are defined, listing 8-62 declares the custom model managers as separate fields in the Store model class. Finally, in listing 8-62 you can see calls made to each of the model managers using the all() method, which return the appropriate filtered results depending on the logic of the backing get_queryset() method.

An alternative to multiple custom model managers, is to create a single custom manager and rely on a custom QuerySet class and methods to execute the same logic, a technique that's illustrated in listing 8-63.

Listing 8-63. Django custom model manager with custom QuerySet class and methods

class StoreQuerySet(models.QuerySet):
    def sandiego(self):
        return self.filter(city='San Diego')
    def losangeles(self):
        return self.filter(city='Los Angeles')

class StoreManager(models.Manager):
    def get_queryset(self):
        return StoreQuerySet(self.model, using=self._db)
    def sandiego(self):
        return self.get_queryset().sandiego()
    def losangeles(self):
        return self.get_queryset().losangeles()

class Store(models.Model):
    name = models.CharField(max_length=30)    
    ...
    objects = models.Manager()
    shops = StoreManager()
Store.shops.all()
Store.shops.sandiego()
Store.shops.losangeles()

The StoreQuerySet class in listing 8-63 is a custom QuerySet class that defines the sandiego() and losangeles() methods, both of which apply additional filters to its base QuerySet. Once you have a QuerySet class, it's necessary to associate it with a custom model manager. In listing 8-63, you can see the StoreManager class represents a custom model manager, which defines its get_queryset() method to set its initial data through the custom StoreQuerySet class.

Next, notice how the custom model manager StoreManager class defines the additional sandiego() and losangeles() methods, which are hooked up to call the methods by the same name in the custom StoreQuerySet class.

Finally, the custom model manager StoreManager is set up as the shops field in the Store model class, where you can observe how calls are made via the shops reference to trigger the query methods backed by the custom StoreQuerySet class.

As helpful as the technique in listing 8-63 is to cut down on the amount model managers, if you look carefully at listing 8-63, there's still a fair amount of redundancy declaring similar named methods for both a custom model manager and a custom QuerySet class.

To cut down on redundant methods when using custom model managers and custom QuerySet classes, the latter type of class offers the as_manager() method to automatically convert a QuerySet class into a custom model manager, a technique that's illustrated in listing 8-64.

Listing 8-64. Django custom model manager with custom QuerySet converted to manager

class StoreQuerySet(models.QuerySet):
    def sandiego(self):
        return self.filter(city='San Diego')
    def losangeles(self):
        return self.filter(city='Los Angeles')

class Store(models.Model):
    name = models.CharField(max_length=30)    
    ...
    objects = models.Manager()
    shops =  StoreQuerySet.as_manager()

Store.shops.all()
Store.shops.sandiego()
Store.shops.losangeles()

The example in listing 8-64 defines the same custom QuerySet class as the one in listing 8-63, however, notice the lack of a custom model manager class. Instead, the Store model definition in listing 8-64 directly references the custom StoreQuerySet class and calls the as_manager() on it to convert the QuerySet class into a model manager. Finally, notice how the calls made via the shops reference are identical to the ones in listing 8-63. In this manner, the technique in listing 8-64 saves you the additional work of creating explicit custom model managers if you're using custom QuerySet classes.

Custom reverse model managers for related models

Earlier in the CRUD relationship records sub-section, you learned how models that have relationships between one another, use reverse queries or _set syntax to execute operations from the model that doesn't have the relationship definition.

These reverse operations are executed by a model manager dubbed RelatedManager which is a subclass of a model's default manager. This means all reverse queries or _set syntax calls are based on the objects model manager reference or whatever default model manager is used by a model.

If you configure a default model manager on a model, then all the reverse operations on a model will automatically use this same manager. However, it's possible to define a custom model manager exclusively for reverse operations, while ignoring the default model manager. This technique consists of explicitly declaring a model manager as part of the reverse operation, as shown in listing 8-65.

Listing 8-65. Django custom model manager for reverse query operations

from django.db import models

class Item(models.Model):
    ...
    objects = models.Manager()  # Default manager for direct queries
    reverseitems = CustomReverseManagerForItems() # Custom Manager for reverse queries

# Get Menu record named Breakfast
breakfast_menu = Menu.objects.get(name='Breakfast')

# Fetch all Item records in the Menu, using Item custom model manager for reverse queries
breakfast_menu.item_set(manager='reverseitems').all() 
# Call on_sale_items() custom manager method in CustomReverseManagerForItems breakfast_menu.item_set(manager='reverseitems').on_sale_items()

Listing 8-65 first declares the Item model with its default objects model manager and a custom model manager assigned to the reverseitems field. Next, a query is made to get a Menu record, followed by various queries to get the Menu record's related Item records via the reverse _set syntax.

However, notice how the reverse query operation in listing 8-65 with _set syntax, uses the manager argument to indicate which model manager to use for reverse operations, in this case, the reverseitems model manager is used to execute the queries, instead of the default objects model manager.