Django: Почему метод get_queryset() менеджера моделей (объектов) выполняется на этапе инициализации?
Я создаю SAAS сайт, который в настоящее время использует django_tenants с postgresql для физического разделения арендаторов на разные схемы. Я решил перенести все в одну схему и внедрить логическое разделение данных вместо этого.
На этапе проверки концепции я добавил новое промежуточное ПО с новой логикой работы с арендаторами, а существующее промежуточное ПО (из django_tenants) оставил на месте. Из моих настроек:
MIDDLEWARE = [
'tenants.middleware.main.TenantMainMiddleware',
'utilities.middleware.TenantMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Существующий из django_tenants - первый: TenantMainMiddleware
- мой новый - следующая строка вниз: TenantMiddleware
В принципе, я добавлю поле ForeignKey
в каждую модель, которой необходимо разделение на арендаторов, и мое промежуточное ПО будет загружать соответствующий экземпляр арендатора на основе поддоменной части домена. Например: demo.mysite.com - вызовет поиск по "demo" в таблице арендаторов. Полученный объект арендатора будет добавлен к объекту запроса, который, в свою очередь, хранится в локальном хранилище потока.
Вот мое промежуточное программное обеспечение:
class TenantMiddleware(MiddlewareMixin):
def process_request(self, request):
current_tenant = connection.tenant # This is temporary to
connection.set_schema_to_public() # allow the existing
subdomain = self.get_subdomain_from_request(request) # tenant code to still work
try:
tenant = VTenant.objects.get(subdomain__iexact=subdomain)
except VTenant.DoesNotExist:
raise APIInvalidRequest(_('No tenant for subdomain "%s".') % subdomain)
request.vtenant = tenant
setattr(_thread_locals, 'request', request)
connection.set_tenant(current_tenant) # Temp code for original middleware
Причина, по которой моя новая модель арендатора названа "VTenant", заключается в том, что она не будет конфликтовать с оригинальной моделью Tenant, которая все еще используется, пока я не закончу проверку концепции.
Я также создал два метода для получения запроса и/или объекта арендатора из _thread_locals
:
def get_current_request():
return getattr(_thread_locals, 'request', None)
def get_current_tenant():
request = get_current_request()
if request:
return getattr(request, 'vtenant', None)
Затем я создал ModelManager с учетом арендаторов, чтобы применять фильтрацию по арендаторам всякий раз, когда get_queryset()
вызывается для модели.
class TenantAwareManager(models.Manager):
def get_queryset(self):
tenant = get_current_tenant()
if not tenant:
raise APIUnexpectedCondition()
return super().get_queryset().filter(tenant=tenant)
И затем все менеджеры для моделей с ограничениями по аренде расширяются из этого класса:
class ClientManager(TenantAwareManager):
def get_by_natural_key(self, name):
return self.get(name__iexact=name)
class Client(TenantModelMixin, models.Model):
objects = ClientManager()
name = models.TextField(
verbose_name=_('Name'),
unique=True,
)
...aditional fields omitted...
TenantModelMixin
просто добавляет поле арендатора в каждую модель. Таким образом, когда я загружаю определенную конечную точку в API сайта, она работает нормально, и фильтрует список Клиентов только до тех, которые принадлежат арендатору в домене ("demo" в данном случае).
Проблема возникает, когда я пытаюсь запустить команды управления. В этих случаях промежуточное ПО не запускается - я полагаю, потому что при выполнении команды управления не происходит фактического запроса. Но на этапе инициализации (компиляции, загрузки, чего угодно), всякий раз, когда ссылаются на объект Client.objects
, выполняется get_queryset()
в TenantAwareManager
и вызывает исключение APIUnexpectedCondition
, потому что объект vtenant не существует (и, фактически, _thread_locals, вероятно, тоже не существует).
Первое место, которое вызвало ошибку, было в классе FilterSet:
class InvoiceFilters(filters.FilterSet):
date_from = filters.DateFilter(field_name='date', lookup_expr='gte')
date_to = filters.DateFilter(field_name='date', lookup_expr='lte')
due_date_from = filters.DateFilter(field_name='due_date', lookup_expr='gte')
due_date_to = filters.DateFilter(field_name='due_date', lookup_expr='lte')
class Meta:
model = Invoice
fields = {
'id': ['exact'],
'number': ['exact', 'iexact'],
...omitted...
'client__name': ['exact', 'iexact', 'icontains'],
}
Нарушающей строкой является фильтр для "client__name". Если я закомментирую ее, то следующее обращение к Client.objects
вызовет исключение. И так далее, до тех пор, пока я буду заботиться (и смогу) закомментировать оскорбительный код.
Я не очень хорошо знаком со всей фазой загрузки/компиляции/инициализации (любой термин подходит), но я не понимаю, почему мой метод get_queryset()
фактически выполняется во время этой фазы. В отладке я поставил точку останова на первой строке кода в моей команде управления, и исключение было вызвано до того, как я достиг этой точки останова.