【问题标题】:Caching queryset choices for ModelChoiceField or ModelMultipleChoiceField in a Django form在 Django 表单中缓存 ModelChoiceField 或 ModelMultipleChoiceField 的查询集选择
【发布时间】:2011-12-31 20:23:48
【问题描述】:

在 Django 表单中使用 ModelChoiceFieldModelMultipleChoiceField 时,有没有办法传入一组缓存的选项?目前,如果我通过 queryset 参数指定选项,则会导致数据库命中。

我想使用 memcached 缓存这些选择,并防止在显示具有此类字段的表单时对数据库进行不必要的访问。

【问题讨论】:

    标签: django django-forms django-admin django-queryset django-cache


    【解决方案1】:

    ModelChoiceField 在生成选择时特别创建命中的原因 - 无论之前是否填充了 QuerySet - 就在于这一行

    for obj in self.queryset.all(): 
    

    django.forms.models.ModelChoiceIterator。正如Django documentation on caching of QuerySets 强调的那样,

    可调用属性每次都会导致数据库查找。

    所以我宁愿只使用

    for obj in self.queryset:
    

    即使我不能 100% 确定这一切的影响(我知道我之后没有关于查询集的大计划,所以我认为没有 .all() 创建的副本我很好)。我很想在源代码中更改它,但由于我将在下次安装时忘记它(而且一开始的风格很糟糕),我最终编写了我的自定义 ModelChoiceField

    class MyModelChoiceIterator(forms.models.ModelChoiceIterator):
        """note that only line with # *** in it is actually changed"""
        def __init__(self, field):
            forms.models.ModelChoiceIterator.__init__(self, field)
    
        def __iter__(self):
            if self.field.empty_label is not None:
                yield (u"", self.field.empty_label)
            if self.field.cache_choices:
                if self.field.choice_cache is None:
                    self.field.choice_cache = [
                        self.choice(obj) for obj in self.queryset.all()
                    ]
                for choice in self.field.choice_cache:
                    yield choice
            else:
                for obj in self.queryset: # ***
                    yield self.choice(obj)
    
    
    class MyModelChoiceField(forms.ModelChoiceField):
        """only purpose of this class is to call another ModelChoiceIterator"""
        def __init__(*args, **kwargs):
            forms.ModelChoiceField.__init__(*args, **kwargs)
    
        def _get_choices(self):
            if hasattr(self, '_choices'):
                return self._choices
    
            return MyModelChoiceIterator(self)
    
        choices = property(_get_choices, forms.ModelChoiceField._set_choices)
    

    这并不能解决数据库缓存的一般问题,但是由于您特别询问ModelChoiceField,而这正是让我首先想到缓存的原因,因此认为这可能会有所帮助。

    【讨论】:

    • 这是一个很棒的解决方案,在 Django 1.8 中完美运行。两个小建议可能会使代码更简洁:1) 您可以从两个类中删除 __init__(),因为它们是无操作的。 2) cache_choices 在 Django 1.9 中被删除,所以你可以去掉整段代码。
    • 嗨,我现在没有使用 Django,所以我没有当前的 Django 设置,因此可以验证这一点。不愿意将其更改为我无法测试的代码 - 您如何使用代码创建答案,然后我在底部编辑此帖子以链接到您的答案?
    【解决方案2】:

    您可以覆盖 QuerySet 中的“all”方法 像

    from django.db import models
    class AllMethodCachingQueryset(models.query.QuerySet):
        def all(self, get_from_cache=True):
            if get_from_cache:
                return self
            else:
                return self._clone()
    
    
    class AllMethodCachingManager(models.Manager):
        def get_query_set(self):
            return AllMethodCachingQueryset(self.model, using=self._db)
    
    
    class YourModel(models.Model):
        foo = models.ForeignKey(AnotherModel)
    
        cache_all_method = AllMethodCachingManager()
    

    然后在使用表单之前更改字段的查询集(例如当您使用表单集时)

    form_class.base_fields['foo'].queryset = YourModel.cache_all_method.all()
    

    【讨论】:

    • 非常感谢!这是最干净的解决方案(据我所知)。在处理具有第三个模型的外键的管理内联模型时非常有用。
    • 这似乎在 Django 1.8 中不起作用。有人可以帮忙吗?
    • @johnny,请参阅 Nicolas78 的答案,它在 Django 1.8 中对我有用。
    • @johnny 我的小技巧适用于 Django 1.10,应该适用于 Django 1.8
    【解决方案3】:

    这是我在 Django 1.10 中使用的一个小技巧,用于在表单集中缓存查询集:

    qs = my_queryset
    
    # cache the queryset results
    cache = [p for p in qs]
    
    # build an iterable class to override the queryset's all() method
    class CacheQuerysetAll(object):
        def __iter__(self):
            return iter(cache)
        def _prefetch_related_lookups(self):
            return False
    qs.all = CacheQuerysetAll
    
    # update the forms field in the formset 
    for form in formset.forms:
        form.fields['my_field'].queryset = qs
    

    【讨论】:

    • 这停止使用 Django 1.11.4 抱怨“AttributeError: 'CacheQuerysetAll' object has no attribute 'all'”。你知道如何解决这个问题吗?谢谢!
    【解决方案4】:

    我在 Django Admin 中使用 InlineFormset 时也偶然发现了这个问题,该 InlineFormset 本身引用了另外两个模型。生成了许多不必要的查询,因为正如Nicolas87 解释的那样,ModelChoiceIterator 每次都从头开始获取查询集。

    可以将以下 Mixin 添加到 admin.ModelAdminadmin.TabularInlineadmin.StackedInline 以将查询数量减少到填充缓存所需的数量。缓存绑定到 Request 对象,因此它在新请求时失效。

     class ForeignKeyCacheMixin(object):
        def formfield_for_foreignkey(self, db_field, request, **kwargs):
            formfield = super(ForeignKeyCacheMixin, self).formfield_for_foreignkey(db_field, **kwargs)
            cache = getattr(request, 'db_field_cache', {})
            if cache.get(db_field.name):
                formfield.choices = cache[db_field.name]
            else:
                formfield.choices.field.cache_choices = True
                formfield.choices.field.choice_cache = [
                    formfield.choices.choice(obj) for obj in formfield.choices.queryset.all()
                ]
                request.db_field_cache = cache
                request.db_field_cache[db_field.name] = formfield.choices
            return formfield
    

    【讨论】:

      【解决方案5】:

      @jnns 我注意到在您的代码中,查询集被评估了两次(至少在我的 Admin 内联上下文中),这似乎是 django admin 的开销,即使没有这个 mixin(当您不使用时,每个内联加一次'没有这种混合)。

      在这个 mixin 的情况下,这是因为 formfield.choices 有一个设置器(为了简化)触发对象的 queryset.all() 的重新评估

      我提出了一项改进,包括直接处理 formfield.cache_choices 和 formfield.choice_cache

      这里是:

      class ForeignKeyCacheMixin(object):
      
          def formfield_for_foreignkey(self, db_field, request, **kwargs):
              formfield = super(ForeignKeyCacheMixin, self).formfield_for_foreignkey(db_field, **kwargs)
              cache = getattr(request, 'db_field_cache', {})
              formfield.cache_choices = True
              if db_field.name in cache:
                  formfield.choice_cache = cache[db_field.name]
              else:
                  formfield.choice_cache = [
                      formfield.choices.choice(obj) for obj in formfield.choices.queryset.all()
                  ]
                  request.db_field_cache = cache
                  request.db_field_cache[db_field.name] = formfield.choices
              return formfield
      

      【讨论】:

      • 不幸的是,它破坏了get_formset注入的自定义查询集
      【解决方案6】:

      这是防止ModelMultipleChoiceField 从数据库重新获取其查询集的另一种解决方案。当您有相同表单的多个实例并且不希望每个表单重新获取相同的查询集时,这很有帮助。此外,查询集是表单初始化的参数,例如在你的视图中定义它。

      请注意,这些类的代码同时发生了变化。此解决方案使用 Django 3.1 的版本。

      这个例子使用了与 Django 的Group 的多对多关系

      models.py

      ​​>
      from django.contrib.auth.models import Group
      from django.db import models
      
      
      class Example(models.Model):
          name = models.CharField(max_length=100, default="")
          groups = models.ManyToManyField(Group)
          ...
      

      forms.py

      ​​>
      from django.contrib.auth.models import Group
      from django import forms
      
      
      class MyModelChoiceIterator(forms.models.ModelChoiceIterator):
          """Variant of Django's ModelChoiceIterator to prevent it from always re-fetching the
          given queryset from database.
          """
      
          def __iter__(self):
              if self.field.empty_label is not None:
                  yield ("", self.field.empty_label)
              queryset = self.queryset
              for obj in queryset:
                  yield self.choice(obj)
      
      
      class MyModelMultipleChoiceField(forms.ModelMultipleChoiceField):
          """Variant of Django's ModelMultipleChoiceField to prevent it from always
          re-fetching the given queryset from database.
          """
      
          iterator = MyModelChoiceIterator
      
          def _get_queryset(self):
              return self._queryset
      
          def _set_queryset(self, queryset):
              self._queryset = queryset
              self.widget.choices = self.choices
      
          queryset = property(_get_queryset, _set_queryset)
      
      
      class ExampleForm(ModelForm):
          name = forms.CharField(required=True, label="Name", max_length=100)
          groups = MyModelMultipleChoiceField(required=False, queryset=Group.objects.none())
      
          def __init__(self, *args, **kwargs):
              groups_queryset = kwargs.pop("groups_queryset", None)
              super().__init__(*args, **kwargs)
              if groups_queryset:
                  self.fields["groups"].queryset = groups_queryset
      
          class Meta:
              model = Example
              fields = ["name", "groups"]
      
      

      views.py

      ​​>
      from django.contrib.auth.models import Group
      from .forms import ExampleForm
      
      
      def my_view(request):
          ...    
          groups_queryset = Group.objects.order_by("name")
          form_1 = ExampleForm(groups_queryset=groups_queryset)
          form_2 = ExampleForm(groups_queryset=groups_queryset)
          form_3 = ExampleForm(groups_queryset=groups_queryset)
          ```
      

      【讨论】:

        【解决方案7】:

        @lai 在 Django 2.1.2 中,我不得不将第一个 if 语句中的代码从 formfield.choice_cache = cache[db_field.name] 更改为 formfield.choices = cache[db_field.name],就像 jnns 的答案一样。在 Django 版本 2.1.2 中,如果您从 admin.TabularInline 继承,您可以直接覆盖方法 formfield_for_foreignkey(self, db_field, request, **kwargs) 而无需使用 mixin。所以代码可能是这样的:

        class MyInline(admin.TabularInline):
            model = MyModel
            formset = MyModelInlineFormset
            extra = 3
        
            def formfield_for_foreignkey(self, db_field, request, **kwargs):
                formfield = super().formfield_for_foreignkey(db_field, request, **kwargs)
                cache = getattr(request, 'db_field_cache', {})
                formfield.cache_choices = True
                if db_field.name in cache:
                    formfield.choices = cache[db_field.name]
                else:
                    formfield.choice_cache = [
                        formfield.choices.choice(obj) for obj in formfield.choices.queryset.all()
                    ]
                    request.db_field_cache = cache
                    request.db_field_cache[db_field.name] = formfield.choices
                return formfield
        

        在我的情况下,我还必须覆盖 get_queryset 才能从 select_related 中受益,如下所示:

        class MyInline(admin.TabularInline):
            model = MyModel
            formset = MyModelInlineFormset
            extra = 3
        
            def formfield_for_foreignkey(self, db_field, request, **kwargs):
                formfield = super().formfield_for_foreignkey(db_field, request, **kwargs)
                cache = getattr(request, 'db_field_cache', {})
                formfield.cache_choices = True
                if db_field.name in cache:
                    formfield.choices = cache[db_field.name]
                else:
                    formfield.choice_cache = [
                        formfield.choices.choice(obj) for obj in formfield.choices.queryset.all()
                    ]
                    request.db_field_cache = cache
                    request.db_field_cache[db_field.name] = formfield.choices
                return formfield
        
            def get_queryset(self, request):
                return super().get_queryset(request).select_related('my_field')
        

        【讨论】:

          猜你喜欢
          • 2011-07-13
          • 1970-01-01
          • 2015-09-09
          • 2020-03-03
          • 1970-01-01
          • 1970-01-01
          • 2014-11-15
          • 2013-11-29
          • 1970-01-01
          相关资源
          最近更新 更多