2
DjangoVersion:2.1.7
PythonVersion:3.8.2

I have a StoreLocation model which has ForeignKeys to Store, City and Site (this is the default DjangoSitesFramework model). I have created TabularInline for StoreLocation and added it to the Store admin. everything works fine and there isn't any problem with these basic parts.

now I'm trying to optimize my admin database queries, therefore I realized that StoreLocationInline will hit database 3 times for each StoreLocation inline data.

I'm using raw_id_fields, managed to override get_queryset and select_related or prefetch_related on StoreLocationInline, StoreAdmin and even in StoreLocationManager, i have tried BaseInlineFormsSet to create custom formset for StoreLocationInline. None of them worked.

Store Model:

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

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

City Model:

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 model with manager:

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 with custom 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')

so when i check Store admin change form, it will do this for each StoreLocationInline item:

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,)

after i have added store to the select_related, the last query for store disappeared. so now i have 2 queries for each StoreLocation.

I have checked the following questions too:


Problem is caused by ForeignKeyRawIdWidget.label_and_url_for_value()

as @AndreyKhoronko mentioned in comments, this problem is caused by ForeignKeyRawIdWidget.label_and_url_for_value() located in django.contrib.admin.widgets which will hit database with that key to generate a link to admin change form.

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
aasmpro
  • 554
  • 9
  • 21
  • Have you tried overriding `get_field_queryset()` on `StoreLocationInline`? – SebCorbin Apr 24 '20 at 14:32
  • Hi @SebCorbin , as i know `get_field_queryset` will be used to get fields ordering if any exists, so how it can affect my problem? may you describe? – aasmpro Apr 25 '20 at 02:46
  • I think, these extra 2 queries per `StoreLocationInline` form are made by `ForeignKeyRawIdWidget.label_and_url_for_value` method on raw id widget rendering to title+link. – Andrey Khoronko Apr 26 '20 at 16:52
  • hi @AndreyKhoronko ! TNX for Ur response, i have checked that function, it seems U R right! so inline will hit database per object to create title+link. i don't see a way to prevent this functionality. any solution? – aasmpro Apr 27 '20 at 07:12
  • @aasmpro you can write custom widget or inherit from `ForeignKeyRawIdWidget` and pass an already loaded from DB instance of `StoreLocation` somehow. And then init your custom widget or field as it shown here: https://docs.djangoproject.com/en/3.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey It's just a propose, I have no complete and tested solution – Andrey Khoronko Apr 27 '20 at 08:01
  • @AndreyKhoronko nice solution, i'm gonna test it and let U know if that worked. anyway tnx, i appreciate Ur response! – aasmpro Apr 27 '20 at 10:10

0 Answers0