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