2016-11-17 108 views
2

我遇到了一些與我的django管理員有關的主要問題。許多重複的查詢基於我有多少內聯。Django Inline for 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')  

編輯:看了你的評論,我意識到我對你的問題的初步解釋是絕對錯誤的。我有多個解決方案,爲您的問題,以及與此去是:

  1. 簡單的一個,我用大部分的時間和建議:只需更換Django的默認選擇部件與raw_id_field部件,沒有查詢製成。只需在內嵌管理員中設置raw_id_fields = ('setting', 'displayed_group')並完成即可。但是,如果你不想擺脫選擇框,我可以給一些半開玩笑的代碼來做這個伎倆,但是很長而且不夠漂亮。這個想法是重寫創建表單的formset併爲formset中的這些字段指定選項,以便僅從數據庫中查詢一次。

這裏有雲:

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,在許多地方像MachineAdmin,ArrangementAdmin,SettingAdmin,ArrangementInline prefetch_related沒有運氣的最後幾天。問題在於選擇/選擇內聯查詢集:每個內聯查詢集對數據庫進行1次查詢以進行「設置」,並對「顯示組」進行1次查詢。如果我有10個內聯,它將有20個查詢。與此同時,MachineAdmin查詢集本身似乎沒有任何影響內聯選擇/選擇查詢集 –

+0

@HBui嗨,索裏我錯誤地解釋了你的問題,我更新了答案。在Django 1.10上測試過。 – makaveli

+0

@makeveli你。是。天才。我發佈了這個問題,希望有人能夠像這樣詳細回答。一切都很完美。我甚至不需要改變任何東西。天才。你真了不起。現在回到最後一個問題:我如何爲您投票或做一些有益於您的幫助?我是新的,所以我不知道那些東西。 –

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