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.
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.
If no manager explicitly created, a generic (models.Manager
) manager will be autocreated and attached with attribute name objects
.
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.
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.)
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
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]
)
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.
Cool! That appears to confirm my conjectures above, and that the docs do say what I thought they said. Thanks for doing that research!
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