@Ilnar861

Как оптимизировать запросы в Django Admin?

Всем привет!

Ситуация такая. Есть проект интернет-магазина на Django.

models.py
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey

class ProductCategory(MPTTModel):

    title = models.CharField(verbose_name=u'Название категории', max_length=100)
    parent = TreeForeignKey('self', verbose_name=u'Родительская категория', on_delete=models.CASCADE, null=True,
                            blank=True,
                            related_name='product_category_children')
    



class Product(models.Model):
    
    title = models.CharField(verbose_name=u'Название товара', max_length=100, )
    description = models.TextField(verbose_name=u'Подробное описание товара', null=True, blank=True)
    price = models.DecimalField(verbose_name=u'Цена товара', max_digits=10, decimal_places=5)
    category = models.ForeignKey(ProductCategory, verbose_name=u'Категория товара', related_name='product_category', on_delete=models.CASCADE, default=None)



class Attribute(models.Model):

    label = models.CharField(verbose_name=u'Название атрибута', max_length=50)

    def __str__(self):
        return self.label


class AttributeValue(models.Model):

    attribute = models.ForeignKey(Attribute, verbose_name=u'Название атрибута', on_delete=models.CASCADE, related_name='attribute_values')
    value = models.CharField(verbose_name=u'Значение атрибута', max_length=255)


    def __str__(self):
        return '{}: {}'.format(self.attribute.label, self.value)



class CategoryAttributeValue(models.Model):

    category = models.ForeignKey(ProductCategory, verbose_name=u'Категория', on_delete=models.PROTECT, null=True, blank=True)
    attribute_value = models.ForeignKey(AttributeValue, verbose_name=u'Атрибут', on_delete=models.PROTECT, null=True, blank=True)


    def __str__(self):
        return '{}: {}'.format(self.attribute_value.attribute.label, self.attribute_value.value)


class ProductAttributeValue(models.Model):

    product_type = models.ForeignKey(Product, verbose_name=u'Товар', on_delete=models.PROTECT, null=True, blank=True, related_name='product_attributes')
    attribute_value = models.ForeignKey(AttributeValue, verbose_name = u'Атрибут товара', on_delete=models.PROTECT, null=True, blank=True, related_name='product_attributes')

    def __str__(self):
        return '{}: {}'.format(self.attribute_value.attribute.label, self.attribute_value.value)


admin.py
from django.contrib import admin
from mptt.admin import DraggableMPTTAdmin
from .models import *
from django.contrib.admin.options import BaseModelAdmin
from django.db.models.constants import LOOKUP_SEP

class AdminBaseWithSelectRelated(BaseModelAdmin):

    list_select_related = []

    def get_queryset(self, request):
        return super(AdminBaseWithSelectRelated, self).get_queryset(request).select_related(*self.list_select_related)

    def form_apply_select_related(self, form):
        for related_field in self.list_select_related:
            splitted = related_field.split(LOOKUP_SEP)

            if len(splitted) > 1:
                field = splitted[0]
                related = LOOKUP_SEP.join(splitted[1:])
                form.base_fields[field].queryset = form.base_fields[field].queryset.select_related(related)



class AdminInlineWithSelectRelated(admin.TabularInline, AdminBaseWithSelectRelated):


    def get_formset(self, request, obj=None, **kwargs):
        formset = super(AdminInlineWithSelectRelated, self).get_formset(request, obj, **kwargs)
        self.form_apply_select_related(formset.form)
        return formset


class AdminWithSelectRelated(admin.ModelAdmin, AdminBaseWithSelectRelated):

    def get_form(self, request, obj=None, **kwargs):
        form = super(AdminWithSelectRelated, self).get_form(request, obj, **kwargs)
        self.form_apply_select_related(form)
        return form




class CategoryAttributeValueInline(AdminInlineWithSelectRelated):
    model = CategoryAttributeValue
    extra = 0
    list_select_related = ['attribute_value__attribute']
    autocomplete_fields = ('attribute_value',)




class ProductAttributeValueInline(AdminInlineWithSelectRelated):
    model = ProductAttributeValue
    extra = 0
    list_select_related = ['attribute_value__attribute']
    autocomplete_fields = ('attribute_value',)



class AttributeValueAdmin(AdminWithSelectRelated):

    list_select_related = ('attribute',)
    search_fields = ('value',)



class ProductCategoryAdmin(DraggableMPTTAdmin):

    list_select_related = ('parent',)
    inlines = [CategoryAttributeValueInline]




class ProductAdmin(admin.ModelAdmin):
    
    inlines = [ProductAttributeValueInline]


admin.site.register(ProductCategory, ProductCategoryAdmin)
admin.site.register(Product, ProductAdmin)
admin.site.register(Attribute)
admin.site.register(AttributeValue, AttributeValueAdmin)


ProductCategory - категории продуктов.

Product - модель продукта, с какими-то общими параметрами (название,описание, цена и т.д.). Также имеет ссылку на категорию.

Модель Attribute - названия характеристики товара (размер, цвет, вес и т.д.).

AttributeValue - значения этих характеристик (черный, белый, 5 кг, 10 кг и т.д.). Имеет связь с Attribute.

Модель CategoryAttributeValue хранит в себе ссылки на ProductCategory и AttributeValue. Нужна собственно для фильтра. Чтобы при переходе на страницу какой-либо категории, в фильтре выводились правильные характеристики. Товары могут быть разные. И характеристики у них тоже разные. Например,у футболок - цвет, размер. У смартфонов - память, диагональ и т.д.

ProductAttributeValue - ссылки на Product и AttributeValue. Для выборки самих товаров по фильтру.

Для удобства редактирования CategoryAttributeValue добавлен к ProductCategory как inline. При запросе к модели категории для редактирования, Silk показывает лишние sql-запросы к связанным моделям.

5e1de3caed3e3628147916.png

По хорошему должен быть только 1 запрос (который 4 ый снизу)
SELECT `catalogue_categoryattributevalue`.`id`,
       `catalogue_categoryattributevalue`.`category_id`,
       `catalogue_categoryattributevalue`.`attribute_value_id`,
       `catalogue_attributevalue`.`id`,
       `catalogue_attributevalue`.`attribute_id`,
       `catalogue_attributevalue`.`value`,
       `catalogue_attribute`.`id`,
       `catalogue_attribute`.`label`
FROM `catalogue_categoryattributevalue`
LEFT OUTER JOIN `catalogue_attributevalue` ON (`catalogue_categoryattributevalue`.`attribute_value_id` = `catalogue_attributevalue`.`id`)
LEFT OUTER JOIN `catalogue_attribute` ON (`catalogue_attributevalue`.`attribute_id` = `catalogue_attribute`.`id`)
WHERE `catalogue_categoryattributevalue`.`category_id` = 2
ORDER BY `catalogue_categoryattributevalue`.`id` ASC


Но он делает еще 6 отдельных запросов с разными айди (сейчас там как раз 6 связанных объектов)

SELECT `catalogue_attributevalue`.`id`,
       `catalogue_attributevalue`.`attribute_id`,
       `catalogue_attributevalue`.`value`,
       `catalogue_attribute`.`id`,
       `catalogue_attribute`.`label`
FROM `catalogue_attributevalue`
INNER JOIN `catalogue_attribute` ON (`catalogue_attributevalue`.`attribute_id` = `catalogue_attribute`.`id`)
WHERE `catalogue_attributevalue`.`id` IN (5)


Классы AdminBaseWithSelectRelated, AdminInlineWithSelectRelated, AdminWithSelectRelated - это решение, которое я нашел здесь. Без них кол-во запросов могло достигать до 100.

При сохранении модели так же много запросов
5e1de76bc4d3c562237931.png

Во фронтенде через select_related() таких проблем нет. Могу предположить, что, чтобы сократить запросы при сохранении (POST), нужно переопределить save()-метод. Но у кого, у модели или формы... А вот где делается эти 6 запросов, не понятно.
  • Вопрос задан
  • 1008 просмотров
Пригласить эксперта
Ответы на вопрос 1
@noremorse_ru
У ModelAdmin есть метод get_queryset, нужно создать дочерний класс и можно туда добавить аргумент чтобы указать кастомный менеджер, ну и написать менеджеры для нужных моделей, соответственно
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы