Base Manager docs are confusing

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.