Django Админ Встроенная база данных попаданий несколько раз - PullRequest
2 голосов
/ 13 апреля 2020
DjangoVersion:2.1.7
PythonVersion:3.8.2

У меня есть модель StoreLocation, которая имеет ForeignKeys для Store, City и Site (это модель DjangoSitesFramework по умолчанию). Я создал TabularInline для StoreLocation и добавил его к администратору Store. все работает нормально, и с этими базовыми c частями проблем нет.

Теперь я пытаюсь оптимизировать запросы к базе данных администратора, поэтому я понял, что StoreLocationInline будет попадать в базу данных 3 раза для каждой StoreLocation встроенные данные.

Я использую raw_id_fields, удалось переопределить get_queryset и select_related или prefetch_related на StoreLocationInline, StoreAdmin и даже в StoreLocationManager, я попытался BaseInlineFormsSet создать пользовательский formset для StoreLocationInline. Ни один из них не работал.

Модель магазина:

class Store(models.AbstractBaseModel):
    name = models.CharField(max_length=50)
    description = models.TextField(blank=True)

    def __str__(self):
        return '[{}] {}'.format(self.id, self.name)

Модель города:

class City(models.AbstractBaseModel, models.AbstractLatitudeLongitudeModel):
    province = models.ForeignKey('locations.Province', on_delete=models.CASCADE, related_name='cities')
    name = models.CharField(max_length=200)
    has_shipping = models.BooleanField(default=False)

    class Meta:
        unique_together = ('province', 'name')

    def __str__(self):
        return '[{}] {}'.format(self.id, self.name)

Модель StoreLocation с менеджером:

class StoreLocationManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().select_related('store', 'city', 'site')


class StoreLocation(models.AbstractBaseModel):
    store = models.ForeignKey('stores.Store', on_delete=models.CASCADE, related_name='locations')
    city = models.ForeignKey('locations.City', on_delete=models.CASCADE, related_name='store_locations')
    site = models.ForeignKey(models.Site, on_delete=models.CASCADE, related_name='store_locations')
    has_shipping = models.BooleanField(default=False)

    # i have tried without manager too
    objects = StoreLocationManager()

    class Meta:
        unique_together = ('store', 'city', 'site')

    def __str__(self):
        return '[{}] {} / {} / {}'.format(self.id, self.store.name, self.city.name, self.site.name)

    # removing shipping_status property does not affect anything for this problem.
    @property
    def shipping_status(self):
        return self.has_shipping and self.city.has_shipping

StoreLocationInline с пользовательским FormSet:

class StoreLocationFormSet(BaseInlineFormSet):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.queryset = self.queryset.select_related('site', 'city', 'store')


class StoreLocationInline(admin.TabularInline):
    model = StoreLocation

    # i have tried without formset and multiple combinations too
    formset = StoreLocationFormset

    fk_name = 'store'
    extra = 1
    raw_id_fields = ('city', 'site')
    fields = ('is_active', 'has_shipping', 'site', 'city')

    # i have tried without get_queryset and multiple combinations too
    def get_queryset(self, request):
        return super().get_queryset(request).select_related('site', 'city', 'store')

StoreAdmin:

class StoreAdmin(AbstractBaseAdmin):
    list_display = ('id', 'name') + AbstractBaseAdmin.default_list_display
    search_fields = ('id', 'name')
    inlines = (StoreLocationInline,)

    # i have tried without get_queryset and multiple combinations too
    def get_queryset(self, request):
        return super().get_queryset(request).prefetch_related('locations', 'locations__city', 'locations__site')

, поэтому, когда я проверяю Store форму изменения администратора, он будет делать это для каждого StoreLocationInline элемента:

DEBUG 2020-04-13 09:32:23,201 [utils:109] (0.000) SELECT `locations_city`.`id`, `locations_city`.`is_active`, `locations_city`.`priority`, `locations_city`.`created_at`, `locations_city`.`updated_at`, `locations_city`.`latitude`, `locations_city`.`longitude`, `locations_city`.`province_id`, `locations_city`.`name`, `locations_city`.`has_shipping`, `locations_city`.`is_default` FROM `locations_city` WHERE `locations_city`.`id` = 110; args=(110,)
DEBUG 2020-04-13 09:32:23,210 [utils:109] (0.000) SELECT `django_site`.`id`, `django_site`.`domain`, `django_site`.`name` FROM `django_site` WHERE `django_site`.`id` = 4; args=(4,)
DEBUG 2020-04-13 09:32:23,240 [utils:109] (0.000) SELECT `stores_store`.`id`, `stores_store`.`is_active`, `stores_store`.`priority`, `stores_store`.`created_at`, `stores_store`.`updated_at`, `stores_store`.`name`, `stores_store`.`description` FROM `stores_store` WHERE `stores_store`.`id` = 22; args=(22,)

после того, как я добавил store к select_related, последний запрос для store исчез. так что теперь у меня есть 2 запроса для каждого StoreLocation.

Я также проверил следующие вопросы:


Проблема вызвана ForeignKeyRawIdWidget.label_and_url_for_value()

, как @AndreyKhoronko упоминается в комментариях эта проблема вызвана тем, что ForeignKeyRawIdWidget.label_and_url_for_value() находится в django.contrib.admin.widgets, который ударит базу данных с этим ключом, чтобы создать ссылку на форму изменения администратора.

class ForeignKeyRawIdWidget(forms.TextInput):
    # ...
    def label_and_url_for_value(self, value):
        key = self.rel.get_related_field().name
        try:
            obj = self.rel.model._default_manager.using(self.db).get(**{key: value})
        except (ValueError, self.rel.model.DoesNotExist, ValidationError):
            return '', ''

        try:
            url = reverse(
                '%s:%s_%s_change' % (
                    self.admin_site.name,
                    obj._meta.app_label,
                    obj._meta.object_name.lower(),
                ),
                args=(obj.pk,)
            )
        except NoReverseMatch:
            url = ''  # Admin not registered for target model.

        return Truncator(obj).words(14, truncate='...'), url
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...