如何在Django Admin中预取ModelInline的ForeignKey?

时间:2018-11-16 02:52:10

标签: python django

例如,我有以下模型:

class Person(models.Model):
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)

class SNS(models.Model):
    name = models.CharField(max_length=255)

    # for display in raw_id_fields
    def __str__(self):
        return self.name

class PersonSNS(models.Model):
    person = models.ForeignKey('Person', related_name='sns')
    sns = models.ForeignKey('SNS')
    url = models.CharField(max_length=255)

admin.py

class PersonSNSInline(admin.StackedInline):
    model = PersonSNS
    fields = ('sns', 'url')
    raw_id_fields = ('sns',)

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    inlines = [PersonSNSInline]

    def get_queryset(self, request):
        return super(PersonAdmin, self).get_queryset(request).prefetch_related('sns')

但是,django-debug-toolbar像下面这样显示执行的SQL

SELECT `sns`.`id`, `sns`.`name` FROM `sns` WHERE `sns`.`id` IN (1, 2, 3, 5, 6)

SELECT `sns`.`id`, `sns`.`name` FROM `sns` WHERE `sns`.`id` = 1
Duplicated 5 times.
SELECT `sns`.`id`, `sns`.`name` FROM `sns` WHERE `sns`.`id` = 5
Duplicated 5 times.
SELECT `sns`.`id`, `sns`.`name` FROM `sns` WHERE `sns`.`id` = 6
Duplicated 5 times. 
SELECT `sns`.`id`, `sns`.`name` FROM `sns` WHERE `sns`.`id` = 3
Duplicated 5 times. 
SELECT `sns`.`id`, `sns`.`name` FROM `sns` WHERE `sns`.`id` = 2
Duplicated 5 times.

我想知道为什么PersonSNSInline尽管我已经预取了相关数据,仍然仍然一个一个地查询数据库。

1 个答案:

答案 0 :(得分:0)

目前不支持

该框架甚至不使用相关的模型管理器来检索内联表单集的查询集。

即使您尝试执行自己的自定义内联和内联表单集,基类的工作方式也非常不利于对象缓存,并且预取/选择相关内容也不会按您预期的那样工作。

  1. 有一些实例 self.get_queryset()[i] 范式内联 /BaseFormaset 创建方法超出了预取的目的。
  2. Baseformset 的 init 包括:
    qs = queryset.filter(**{self.fk.name: self.instance})

为了能够真正创建支持这一点的自定义内联,框架需要:

  1. 通过 ge_instance_by_index 方法中的索引提取实例,以便我们可以实现管理员在当前基类的顶部执行我们想做的事情。如果这样做,那么我们将能够覆盖 ge_instance_by_index 以在支持预取的内联上返回 list(self.get_queryset())[i] 而不是 self.get_queryset()[i]
  2. BaseInlineFormSet 构造函数中删除查询集准备逻辑。例如,如果查询集是用如下方法准备的,我们可以覆盖它以保持相关的管理器查询集完整:

def prepare_queryset(queryset):
       if queryset is None:
           queryset = self.model._default_manager
       if self.instance.pk is not None:
           qs = queryset.filter(**{self.fk.name: self.instance})
       else:
           qs = queryset.none()
       return qs

会变得只是

def prepare_queryset(queryset):
   return  queryset
  1. 在获取查询集中提供对象,以便我们可以从所需的相关管理器获取查询集

证明如果上述问题得到修复,我们可以获得可用的自定义解决方案的黑客方法是:

class RelatedManagerInlineFormSet(BaseInlineFormSet):
    """
    -------hack around issue 2-------
    """ 
    def get_queryset(self):
        return list(self.queryset)
    @property
    def queryset(self):
        return self._related_manager_queryset

    @queryset.setter
    def queryset(self, v):
        #keeps the property readonly
        pass


    def __init__(self,*args, **kwargs) -> None:
        self._related_manager_queryset=kwargs['queryset']
        kwargs['queryset']=None
        super().__init__(*args, **kwargs)

class RelatedModelManagerInline(BaseInlineFormSet):
    """Relate manager inline mixin to be used
    """
    formset=RelatedManagerInlineFormSet
    def get_formset(self, request: HttpRequest, obj=None, *args, **kwargs):
        """
         -------hack around issue 3-------
        """ 
        if obj is not None:
            self.obj=obj
        return super().get_formset(request, obj=obj, *args, **kwargs)

    def get_queryset(self, request: HttpRequest) -> list:
        """
        Returns a list for related manager's queryset, although dangerous
        -------returning a list is a hack around issue 1-------
        add 'related_name' property to the class that extends this
        Args:
            request (HttpRequest): the request being performed

        Returns:
            list: the list returned from related queryset
        """
        return list(getattr(self.obj, self.related_name).all())

请不要使用。以上只是表明如果问题得到解决,我们可以有一个自定义的内联支持预取

我个人认为预取支持应该是一个内置功能,因为广泛的讨论证明社区希望它作为默认行为,因此我在这里创建了一张票: https://code.djangoproject.com/ticket/32587