11

Khi sử dụng ModelChoiceField hoặc ModelMultipleChoiceField trong một hình thức Django, là có một cách để vượt qua trong một bộ cache của sự lựa chọn? Hiện tại, nếu tôi chỉ định các lựa chọn thông qua tham số truy vấn, nó sẽ dẫn đến một lần truy cập cơ sở dữ liệu.Caching queryset sự lựa chọn cho ModelChoiceField hoặc ModelMultipleChoiceField trong một hình thức Django

Tôi muốn lưu bộ nhớ cache những lựa chọn này bằng cách ghi nhớ và ngăn các lần truy cập không cần thiết vào cơ sở dữ liệu khi hiển thị biểu mẫu với trường như vậy.

Trả lời

9

Lý do mà ModelChoiceField đặc biệt tạo ra một hit khi tạo sự lựa chọn - bất kể QuerySet đã được dân cư trước đó - nằm trong dòng này

for obj in self.queryset.all(): 

trong django.forms.models.ModelChoiceIterator. Khi Django documentation on caching of QuerySets nổi bật,

callable attributes cause DB lookups every time.

Vì vậy, tôi muốn chỉ cần sử dụng

for obj in self.queryset: 

mặc dù tôi không chắc chắn 100% về tất cả những tác động của việc này (tôi biết tôi không có lớn kế hoạch với queryset sau đó, vì vậy tôi nghĩ rằng tôi là tốt mà không có bản sao .all() tạo ra). Tôi đang bị cám dỗ để thay đổi điều này trong mã nguồn, nhưng kể từ khi tôi sẽ quên nó ở bên cạnh cài đặt (và đó là phong cách xấu để bắt đầu với) tôi đã kết thúc bằng văn bản tùy chỉnh của tôi 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) 

Điều này không giải quyết được vấn đề chung của bộ nhớ đệm cơ sở dữ liệu, nhưng vì bạn đang hỏi về ModelChoiceField nói riêng và đó chính là điều khiến tôi nghĩ về bộ nhớ đệm đó ngay từ đầu, nghĩ rằng điều này có thể hữu ích.

+1

Đây là một giải pháp tuyệt vời và hoạt động hoàn hảo trong Django 1.8. Hai gợi ý nhỏ có thể làm cho mã hơi sạch hơn: 1) Bạn có thể loại bỏ '__init __()' khỏi cả hai lớp, vì chúng không có op. 2) 'cache_choices' bị xóa trong Django 1.9, vì vậy bạn có thể loại bỏ toàn bộ đoạn mã đó ra. – Chad

+0

Xin chào, tôi không sử dụng Django tại thời điểm này vì vậy tôi không có Django hiện tại thiết lập và do đó cách để xác minh điều này. Bất đắc dĩ để thay đổi mã này thành mã tôi không thể kiểm tra - bạn nghĩ gì về việc tạo câu trả lời bằng mã và tôi chỉnh sửa bài đăng này ở cuối để liên kết với câu trả lời của bạn? – Nicolas78

12

Bạn có thể ghi đè lên "tất cả" phương pháp trong QuerySet cái gì đó như

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() 

Và sau đó thay đổi queryset của trường trước khi hình thức sử dụng (đối với exmple khi bạn sử dụng formsets)

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

cảm ơn tuyệt vời! Đây là giải pháp sạch nhất (theo hiểu biết tốt nhất của tôi). Rất hữu ích khi làm việc với các mô hình quản trị nội tuyến có ForeignKeys cho các mô hình thứ ba. – ppetrid

+1

Điều này dường như không hoạt động ở Django 1.8. Có ai giúp được không? – johnny

+0

@ johnny, xem câu trả lời của Nicolas78, hoạt động cho tôi ở Django 1.8. – Chad

2

Tôi cũng vấp trên vấn đề này trong khi sử dụng một InlineFormset trong Admin Django rằng chính nó tham chiếu hai mô hình khác. Rất nhiều truy vấn không cần thiết được tạo ra bởi vì, như được giải thích Nicolas87, ModelChoiceIterator tìm nạp bộ truy vấn mọi lúc từ đầu.

Mixin sau đây có thể được thêm vào admin.ModelAdmin, admin.TabularInline hoặc admin.StackedInline để giảm số lượng truy vấn chỉ là những truy vấn cần thiết để lấp đầy bộ nhớ cache. Bộ nhớ cache được gắn với đối tượng Request, do đó nó sẽ vô hiệu hóa trên một yêu cầu mới.

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 
2

@jnns tôi nhận thấy rằng trong mã của bạn queryset được đánh giá hai lần (ít nhất là trong bối cảnh inline quản trị của tôi), mà dường như là một chi phí của django quản trị dù sao, ngay cả khi không mixin này (cộng với một lần mỗi nội tuyến khi bạn không có sự pha trộn này).

Trong trường hợp của mixin này, điều này là do thực tế là formfield.choices có một setter (để đơn giản) kích hoạt việc đánh giá lại bộ truy vấn của đối tượng.tất cả()

Tôi đề nghị một sự cải tiến trong đó bao gồm việc trực tiếp với formfield.cache_choices và formfield.choice_cache

Ở đây là:

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 
3

Dưới đây là một hack chút tôi sử dụng với Django 1,10 đến cache một queryset trong một formset:

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 
+0

Điều này ngừng làm việc với Django 1.11.4 phàn nàn "AttributeError: 'CacheQuerysetAll' đối tượng không có thuộc tính 'tất cả'". Bạn có bất kỳ ý tưởng làm thế nào để sửa lỗi này? Cảm ơn! – hetsch

Các vấn đề liên quan