2016-11-17 3 views
2

Я столкнулся с одной важной проблемой с моим администратором django. Множество повторяющихся запросов, основанных на количестве встроенных строк, которые у меня есть.Django Inline для ManyToMany генерирует повторяющиеся запросы

models.py

class Setting(models.Model): 
    name = models.CharField(max_length=50, unique=True) 

    class Meta: 
     ordering = ('name',) 

    def __str__(self): 
     return self.name 


class DisplayedGroup(models.Model): 
    name = models.CharField(max_length=30, unique=True) 
    position = models.PositiveSmallIntegerField(default=100) 

    class Meta: 
     ordering = ('priority',) 

    def __str__(self): 
     return self.name 


class Machine(models.Model): 
    name = models.CharField(max_length=20, unique=True) 
    settings = models.ManyToManyField(
     Setting, through='Arrangement', blank=True 
    ) 

    class Meta: 
     ordering = ('name',) 

    def __str__(self): 
     return self.name 


class Arrangement(models.Model): 
    machine = models.ForeignKey(Machine, on_delete=models.CASCADE) 
    setting = models.ForeignKey(Setting, on_delete=models.CASCADE) 
    displayed_group = models.ForeignKey(
     DisplayedGroup, on_delete=models.PROTECT, 
     default=1) 
    priority = models.PositiveSmallIntegerField(
     default=100, 
     help_text='Smallest number will be displayed first' 
    ) 

    class Meta: 
     ordering = ('priority',) 
     unique_together = (("machine", "setting"),) 

admin.py

class ArrangementInline(admin.TabularInline): 
    model = Arrangement 
    extra = 1 


class MachineAdmin(admin.ModelAdmin): 
    inlines = (ArrangementInline,) 

Если у меня есть 3 настройки добавлены в инлайн форме и 1 дополнительный, у меня есть около 10 Дубликат запросов

SELECT "corps_setting"."id", "corps_setting"."name", "corps_setting"."user_id", "corps_setting"."tagged", "corps_setting"."created", "corps_setting"."modified" FROM "corps_setting" ORDER BY "corps_setting"."name" ASC 
- Duplicated 5 times 

SELECT "corps_displayedgroup"."id", "corps_displayedgroup"."name", "corps_displayedgroup"."color", "corps_displayedgroup"."priority", "corps_displayedgroup"."created", "corps_displayedgroup"."modified" FROM "corps_displayedgroup" ORDER BY "corps_displayedgroup"."priority" ASC 
- Duplicated 5 times. 

Может кто-нибудь, пожалуйста, скажите мне, что я делаю неправильно прямо здесь? Я потратил 3 дня, пытаясь решить проблему самостоятельно без везения.

Проблема ухудшается, когда у меня около 50 настроек встроенных машин, у меня будет ~ 100 запросов.

Here is the screenshot

ответ

1

Это в значительной степени нормальное поведение в Django - это не делает оптимизацию для вас, но это дает вам достойные инструменты, чтобы сделать это самостоятельно. И не потеть, 100 запросов на самом деле не большая проблема (я видел 16k запросов на одной странице), которые нуждаются в исправлении сразу. Но если ваши объемы данных будут быстро расти, тогда разумно иметь дело с этим, конечно.

Основным оружием, на котором вы будете вооружены, являются методы набора запросов select_related() и prefetch_related(). Там действительно нет никакого смысла идти слишком глубоко в них, так как они очень хорошо документированы here, но только общий указатель:

  • использование select_related() когда объект вы запрашивая имеет только один связанный объект (FK или One2One)

  • использование prefetch_related() когда объект вы запрашивая имеет несколько связанных объектов (на другой конец FK или M2M)

и как использовать их в Django администратора, спросите вы? Элементарный, дорогой Ватсон. Переопределить метод администратора страницы get_queryset(self, request) так это будет выглядеть н так:

from django.contrib import admin 

class SomeRandomAdmin(admin.ModelAdmin): 
    def get_queryset(self, request): 
     return super().get_queryset(request).select_related('field1', 'field2').prefetch_related('field3')  

EDIT: Прочитав ваш комментарий, я понимаю, что мое первоначальное толкование вашего вопроса было абсолютно неправильно. У меня есть несколько решений для вашей проблемы, а также и здесь идет, что:

  1. Простые один, который я использую большую часть времени и рекомендовать: просто заменить Django по умолчанию выберите виджеты с raw_id_field виджетами и никаких запросов сделаны , Просто установите raw_id_fields = ('setting', 'displayed_group') в встроенный администратор и сделайте это для.

  2. Но если вы не хотите избавляться от ящиков выбора, я могу дать некоторый полу-хакерский код, который делает трюк, но довольно длинный и не очень красивый. Идея состоит в том, чтобы переопределить набор форм, который создает формы и задает выбор для этих полей в наборе форм, чтобы они были запрошены только один раз из базы данных.

Вот он идет:

from django import forms 
from django.contrib import admin 
from app.models import Arrangement, Machine, Setting, DisplayedGroup 


class ChoicesFormSet(forms.BaseInlineFormSet): 
    setting_choices = list(Setting.objects.values_list('id', 'name')) 
    displayed_group_choices = list(DisplayedGroup.objects.values_list('id', 'name')) 

    def _construct_form(self, i, **kwargs): 
     kwargs['setting_choices'] = self.setting_choices 
     kwargs['displayed_group_choices'] = self.displayed_group_choices 
     return super()._construct_form(i, **kwargs) 


class ArrangementInlineForm(forms.ModelForm): 
    class Meta: 
     model = Arrangement 
     exclude =() 

    def __init__(self, *args, **kwargs): 
     setting_choices = kwargs.pop('setting_choices', [((),())]) 
     displayed_group_choices = kwargs.pop('displayed_group_choices', [((),())]) 

     super().__init__(*args, **kwargs) 

     # This ensures that you can still save the form without setting all 50 (see extra value) inline values. 
     # When you save, the field value is checked against the "initial" value 
     # of a field and you only get a validation error if you've changed any of the initial values. 
     self.fields['setting'].choices = [('-', '---')] + setting_choices 
     self.fields['setting'].initial = self.fields['setting'].choices[0][0] 
     self.fields['setting'].empty_values = (self.fields['setting'].choices[0][0],) 

     self.fields['displayed_group'].choices = displayed_group_choices 
     self.fields['displayed_group'].initial = self.fields['displayed_group'].choices[0][0] 


class ArrangementInline(admin.TabularInline): 
    model = Arrangement 
    extra = 50 
    form = ArrangementInlineForm 
    formset = ChoicesFormSet 

    def get_queryset(self, request): 
     return super().get_queryset(request).select_related('setting') 


class MachineAdmin(admin.ModelAdmin): 
    inlines = (ArrangementInline,) 


admin.site.register(Machine, MachineAdmin) 

Если вы нашли что-то, что можно было бы улучшить или какие-либо вопросы, дайте мне знать.

+0

Я пытался использовать select_related, prefetch_related в многочисленных местах, как MachineAdmin, ArrangementAdmin, SettingAdmin, ArrangementInline без удачи в течение последних нескольких дней. Проблема заключается в наборе запросов Select/Choices inline: с каждым встроенным, он делает 1 запрос к базе данных для «настройки» и 1 запрос для «отображаемой группы». Если у меня 10 строк, у нас будет 20 запросов. Между тем, сам запрос набора MachineAdmin, похоже, не влияет на встроенный запрос выбора/выбора –

+0

@HBui Эй, сорри, я неверно истолковал ваш вопрос, я обновил ответ. Протестировано на Django 1.10. – makaveli

+0

@makeveli Вы. Находятся. Гений. Я разместил этот вопрос, не надеясь, что кто-то сможет ответить в деталях, как это. Все работает отлично. Мне даже не пришлось ничего менять. Genius. Ты великолепен. Теперь на последний вопрос: как я могу проголосовать за вас или сделать что-то полезное для вашей помощи? Я новичок, поэтому я не знаю этого. –

4

Я собрал общее решение, основываясь на @ Makaveli отвечают, что, кажется, не имеют проблемы, упомянутые в комментариях:

class CachingModelChoicesFormSet(forms.BaseInlineFormSet): 
    """ 
    Used to avoid duplicate DB queries by caching choices and passing them all the forms. 
    To be used in conjunction with `CachingModelChoicesForm`. 
    """ 

    def __init__(self, *args, **kwargs): 
     super().__init__(*args, **kwargs) 
     sample_form = self._construct_form(0) 
     self.cached_choices = {} 
     try: 
      model_choice_fields = sample_form.model_choice_fields 
     except AttributeError: 
      pass 
     else: 
      for field_name in model_choice_fields: 
       if field_name in sample_form.fields and not isinstance(
        sample_form.fields[field_name].widget, forms.HiddenInput): 
        self.cached_choices[field_name] = [c for c in sample_form.fields[field_name].choices] 

    def get_form_kwargs(self, index): 
     kwargs = super().get_form_kwargs(index) 
     kwargs['cached_choices'] = self.cached_choices 
     return kwargs 


class CachingModelChoicesForm(forms.ModelForm): 
    """ 
    Gets cached choices from `CachingModelChoicesFormSet` and uses them in model choice fields in order to reduce 
    number of DB queries when used in admin inlines. 
    """ 

    @property 
    def model_choice_fields(self): 
     return [fn for fn, f in self.fields.items() 
      if isinstance(f, (forms.ModelChoiceField, forms.ModelMultipleChoiceField,))] 

    def __init__(self, *args, **kwargs): 
     cached_choices = kwargs.pop('cached_choices', {}) 
     super().__init__(*args, **kwargs) 
     for field_name, choices in cached_choices.items(): 
      if choices is not None and field_name in self.fields: 
       self.fields[field_name].choices = choices 

Все, что вам нужно сделать, это подкласс вашей модели от CachingModelChoicesForm и использовать CachingModelChoicesFormSet в своем классе инлайн:

class ArrangementInlineForm(CachingModelChoicesForm): 
    class Meta: 
     model = Arrangement 
     exclude =() 


class ArrangementInline(admin.TabularInline): 
    model = Arrangement 
    extra = 50 
    form = ArrangementInlineForm 
    formset = CachingModelChoicesFormSet 
Смежные вопросы