Base Manager docs are confusing

Screenshot of 3.2 docs:

“Django uses an instance of the Model._base_manager manager class when accessing related objects…”

“Base managers aren’t used when querying on related models…”

I’m so confused.

I agree - it took me a couple of readings to come to a conclusion as to what I think is being said here, and it comes down to the difference between object access and a query.

In other words, choice.question is object access. Given an instance of Choice named choice, choice.question is a reference to a Question object. There’s no query being used here - you have the object and you’re accessing a related object. In this case, the base manager is used for accessing that related object.

On the other hand, the expression Choice.objects.filter(question__name__startswith='What') is a query. It creates an SQL statement to be issued to the database. In this case, the base manager is not used.

At least that’s how I am reading this. I could well be wrong…

I get that accessing a OneToOne or ManyToOne descriptor, which is expected to resolve to a Model instance, is different than accessing a somethingToMany descriptor, which resolves to a QuerySet instance.

However, even in the former case, a query is still generated under the hood to fetch that single Model instance.

I’m gonna continue running experiments and see if I can reverse-engineer a complete understanding of Django manager behavior and attributes through brute force.

After much experimentation and skimming the source (v3.2), I believe I have figured out the important parts.

  1. If no manager explicitly created, a generic (models.Manager) manager will be autocreated and attached with attribute name objects.

  2. There exists two Meta class options named base_manager_name and default_manager_name that are not set by default. If set, they will be accessible as _meta attributes.

  3. There exists three _meta attributes that aggregate managers:
    a. _meta.managers - all managers (list)
    b. _meta.managers_map - all managers indexed by manager.name (dict)
    c. _meta.local_managers - guessing this is managers in current class (not base classes)
    NOTE: I have no idea how these get populated - something to do with a mechanism called contribute_to_class, but I stopped following that trail. Seems to include all explicitly created managers plus autocreated default managers. (Not autocreated base managers.)

  4. There exists a _meta attribute named base_manager which is a @cached_property:
    a. If _meta.base_manager_name set, will attempt to return _meta.managers_map[base_manager_name]
    b. else, creates and returns generic manager with name set to _base_manager

  5. There exists a _meta attribute named default_manager which is a @cached_property:
    a. If _meta.default_manager_name set, will attempt to return _meta.managers_map[default_manager_name]
    b. else, returns first manager (_meta.managers[0])

  6. There exists two Model class attributes named _base_manager and _default_manager that are properties (@property) that simply return _meta.base_manager and _meta.default_manager, respectively

Which leaves the question - what are the default and base managers used for? Obviously, when explicitly referencing them (Foo.objects…), you control which one you’re using. So, the question is, which one does Django use - specifically when dealing with relations.

To test that, I created the following example:

class InvestorManager1(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(deleted=True)

class InvestorManager2(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(deleted=False)

class Investor(models.Model):
    advisors = models.ManyToManyField( "Advisor", related_name="investors", through="Connection" )

    name    = models.CharField(max_length=30)
    deleted = models.BooleanField(default=False)

    m1 = InvestorManager1()
    m2 = InvestorManager2()

    class Meta:
        base_manager_name    = 'm1'
        default_manager_name = 'm2'

class Advisor(models.Model):
    name    = models.CharField(max_length=30)
    deleted = models.BooleanField(default=False)

class Connection(models.Model):
    investor = models.ForeignKey( Investor, on_delete=models.CASCADE, related_name="connections" )
    advisor  = models.ForeignKey( Advisor,  on_delete=models.CASCADE, related_name="connections" )

And, after temporarily commenting out the manager lines, created records:

a = Investor.objects.create(name="Jane Not-Deleted")
b = Investor.objects.create(name="Joe Deleted", deleted=True)
x = Advisor.objects.create(name="Frank", deleted=True)
c1 = Connection.objects.create(investor=a, advisor=x)
c2 = Connection.objects.create(investor=b, advisor=x)

Then ran the tests:

x.investors.all() - Yielded Jane only, which means Django used the default manager (m2) to resolve a relational queryset.

c1.investor - Raised Investor.DoesNotExist error, which means Django used the base manager (m1) to resolve a relational object.
c2.investor - Yielded Joe, confirming the same conclusion.

2 Likes

Cool! That appears to confirm my conjectures above, and that the docs do say what I thought they said. Thanks for doing that research!

1 Like

I find this more palatable than the documentation to discuss Managers (_Base_manager I don’t know about though):

https://django-book.readthedocs.io/en/latest/chapter10.html#managers

It’s a good one to bookmark as it gives a lot of examples, this “Django book” is basically information taken from “Django Book, the comprehensive guide to Django”, I found this resource helpful on more than one occasion

1 Like