Как работают вызовы models.Manager и пользовательских менеджеров?
Следующий вопрос был задан другим пользователем, который впоследствии удалил вопрос. Но я считаю полезным немного "покопаться" в том, как работает логика менеджера Django.
Я расширил класс
models.Manager
и создал собственный менеджер.class PublishedManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(status=Post.Status.PUBLISHED)
Это более чем понятно, но как работают вызовы менеджеров?
objects = models.Manager() # The default manager. published = PublishedManager() # Our custom manager.
Я не обращаюсь к методу
get_queryset()
, я просто вызываю конструкторы. Тогда как это работает?
It does not need to, the .all()
[Django-doc], .get(…)
[Django-doc], etc. all behind the curtains call get_queryset()
and then perform that method on the result [GitHub]:
class BaseManager: # ... @classmethod def _get_queryset_methods(cls, queryset_class): def create_method(name, method): @wraps(method) def manager_method(self, *args, **kwargs): return getattr(self.get_queryset(), name)(*args, **kwargs) return manager_method new_methods = {} for name, method in inspect.getmembers( queryset_class, predicate=inspect.isfunction ): # Only copy missing methods. if hasattr(cls, name): continue # Only copy public methods or methods with the attribute # queryset_only=False. queryset_only = getattr(method, "queryset_only", None) if queryset_only or (queryset_only is None and name.startswith("_")): continue # Copy the method onto the manager. new_methods[name] = create_method(name, method) return new_methods
Он проверяет методы класса queryset
, который он будет оборачивать. Теперь, если QuerySet
имеет метод .all()
, он создаст небольшую функцию .all()
для менеджера, которая сначала вызовет get_queryset()
менеджера, а затем .all()
на кверисете. Он делает это для всех функций, определенных в классе queryset, и таким образом создает для каждой из них функцию для менеджера.
Затем они вставляются как члены в Manager
с [GitHub]:
class BaseManager: # ... @classmethod def from_queryset(cls, queryset_class, class_name=None): if class_name is None: class_name = "%sFrom%s" % (cls.__name__, queryset_class.__name__) return type( class_name, (cls,), { "_queryset_class": queryset_class, **cls._get_queryset_methods(queryset_class), }, )
But there is something else that plays here: how does the manager knows what the model is? Django's models call .contribute_to_class(…)
on every attribute in the class, if it is available, for a manager, that looks like [GitHub]:
class BaseManager: # ... def contribute_to_class(self, cls, name): self.name = self.name or name self.model = cls setattr(cls, name, ManagerDescriptor(self)) cls._meta.add_manager(self)
Таким образом, модель внедряется в менеджер, который даже не был известен на момент создания элемента. При этом не устанавливает сам менеджер на класс модели, а дескриптор его.
Тогда дескриптор будет [делегировать получение атрибута менеджеру:
class ManagerDescriptor: # ... def __get__(self, instance, cls=None): if instance is not None: raise AttributeError( "Manager isn't accessible via %s instances" % cls.__name__ ) if cls._meta.abstract: raise AttributeError( "Manager isn't available; %s is abstract" % (cls._meta.object_name,) ) if cls._meta.swapped: raise AttributeError( "Manager isn't available; '%s' has been swapped for '%s'" % ( cls._meta.label, cls._meta.swapped, ) ) return cls._meta.managers_map[self.manager.name]
Это предотвратит использование my_book.objects
, и, таким образом, вы сможете использовать только Book.objects
, а также предотвратит вызов менеджера на абстрактных моделях.