Django 管理员内联:select_related

Posted

技术标签:

【中文标题】Django 管理员内联:select_related【英文标题】:Django admin inline: select_related 【发布时间】:2015-06-21 05:57:20 【问题描述】:

在带有模型的 Python 3.4.1 上使用 Django 1.8:

class Product(models.Model):
    name = models.CharField(max_length=255)
    # some more fields here

    def __str__(self):
        return self.name


class PricedProduct(models.Model):
    product = models.ForeignKey(Product, related_name='prices')
    # some more fields here

    def __str__(self):
        return str(self.product)

class Coming(models.Model):
    # some unimportant fields here


class ComingProducts(models.Model):
    coming = models.ForeignKey(Coming)
    priced_product = models.ForeignKey(PricedProduct)
    # more unimportant fields

和下面的 admin.py:

class ComingProductsInline(ForeignKeyCacheMixin, admin.TabularInline):
    model = ComingProducts


class ComingAdmin(admin.ModelAdmin):
    inlines = [ComingProductsInline]

当然,我在对数据库进行多次查询时遇到了问题:我对列表中的每个项目都有一个查询,对每一行都有一个查询。所以,有 100 个项目我得到 100 ^ 2 个查询。 我用Caching queryset choices for ModelChoiceField or ModelMultipleChoiceField in a Django form 解决了每一行的查询问题 但我仍然对 str 方法有问题。我尝试了以下方法:

1) 将 prefetch_related 添加到 ComingAdmin:

def get_queryset(self, request):
    return super(ComingAdmin, self).get_queryset(request). \
    prefetch_related('products__product')

2) 将 select_related 添加到 ComingProductInline:

def get_queryset(self, request):
    return super(ComingProductsInline, self).get_queryset(request). \
    select_related('priced_product__product')

3) 为内联定义自定义表单并将 select_related 添加到字段查询集:

 class ComingProductsInline(ForeignKeyCacheMixin, admin.TabularInline):
     model = ComingProducts
     form = ComingProductsAdminForm

 class ComingProductsAdminForm(ModelForm):
     def __init__(self, *args, **kwargs):
              super(ComingProductsAdminForm, self).__init__(args, kwargs)
              self.fields['priced_product'].queryset = PricedProduct.objects.all(). \
              select_related('product')

     class Meta:
         model = ComingProducts
         fields = '__all__'

4) 定义自定义表单集:

 class ComingProductsInline(ForeignKeyCacheMixin, admin.TabularInline):
     model = ComingProducts
     formset = MyInlineFormset

 class MyInlineFormset(BaseInlineFormSet):
     def __init__(self, data=None, files=None, instance=None,
             save_as_new=False, prefix=None, queryset=None, **kwargs):
        super(MyInlineFormset, self).__init__(data, files, instance,
                                          save_as_new, prefix, queryset, **kwargs)
        self.queryset = ComingProducts.objects.all(). \
        prefetch_related('priced_product__product')

5) 前 4 种方法的不同组合

没有任何帮助:每次调用 str 对 PricedProduct 都会使 Django 执行对 Product 表的查询。所有这些方法都在 *** 上提到过,但是它们处理了 ModelAdmin,并且对 Inline 没有帮助。我错过了什么?

【问题讨论】:

【参考方案1】:

受@helpse 回答的启发,如果您只想覆盖单个管理员内联的查询集,还可以执行以下操作:

class ComingProductsInline(admin.TabularInline):
    model = ComingProducts

    def get_formset(self, request, obj=None, **kwargs):
        formset = super(ComingProductsInline, self).get_formset(request, obj, **kwargs)
        queryset = formset.form.base_fields["priced_product"].queryset
        queryset = queryset.select_related("product")
        formset.form.base_fields["priced_product"].queryset = queryset
        return formset

对于大多数情况来说,这可能就足够了。

【讨论】:

【参考方案2】:

您会发现这种方法非常有用:

project/admin.py

from django.contrib import admin
from django.contrib.admin.options import BaseModelAdmin
from django.db.models.constants import LOOKUP_SEP


class AdminBaseWithSelectRelated(BaseModelAdmin):
    """
    Admin Base using list_select_related for get_queryset related fields
    """
    list_select_related = []

    def get_queryset(self, request):
        return super(AdminBaseWithSelectRelated, self).get_queryset(request).select_related(*self.list_select_related)

    def form_apply_select_related(self, form):
        for related_field in self.list_select_related:
            splitted = related_field.split(LOOKUP_SEP)

            if len(splitted) > 1:
                field = splitted[0]
                related = LOOKUP_SEP.join(splitted[1:])
                form.base_fields[field].queryset = form.base_fields[field].queryset.select_related(related)


class AdminInlineWithSelectRelated(admin.TabularInline, AdminBaseWithSelectRelated):
    """
    Admin Inline using list_select_related for get_queryset and get_formset related fields
    """

    def get_formset(self, request, obj=None, **kwargs):
        formset = super(AdminInlineWithSelectRelated, self).get_formset(request, obj, **kwargs)

        self.form_apply_select_related(formset.form)

        return formset


class AdminWithSelectRelated(admin.ModelAdmin, AdminBaseWithSelectRelated):
    """
    Admin using list_select_related for get_queryset and get_form related fields
    """

    def get_form(self, request, obj=None, **kwargs):
        form = super(AdminWithSelectRelated, self).get_form(request, obj, **kwargs)

        self.form_apply_select_related(form)

        return form


class FilterWithSelectRelated(admin.RelatedFieldListFilter):
    list_select_related = []

    def field_choices(self, field, request, model_admin):
        return [
            (getattr(x, field.remote_field.get_related_field().attname), str(x))
            for x in self.get_queryset(field)
        ]

    def get_queryset(self, field):
        return field.remote_field.model._default_manager.select_related(*self.list_select_related)

app/admin.py

from django.contrib import admin

from project.admin import AdminWithSelectRelated, AdminInlineWithSelectRelated, FilterWithSelectRelated
from .models import FormaPago, Comprobante, ItemServicio, ItemBazar


class ItemServicioInlineAdmin(AdminInlineWithSelectRelated):
    model = ItemServicio

    list_select_related = (
        'alumno_servicio__alumno__estudiante__profile',
        'alumno_servicio__servicio__grado',
        'comprobante__forma_pago',
    )


class ItemBazarInlineAdmin(AdminInlineWithSelectRelated):
    model = ItemBazar

    list_select_related = (
        'alumno_item__alumno__estudiante__profile',
        'alumno_item__item__anio_lectivo',
        'comprobante__forma_pago',
    )


class ComprobanteAdmin(AdminWithSelectRelated):
    list_display = ('__str__', 'total', 'estado', 'fecha_generado', 'forma_pago', 'tipo', )
    list_filter = ('estado', 'forma_pago', )

    list_select_related = ('forma_pago', )
    inlines = (ItemServicioInlineAdmin, ItemBazarInlineAdmin, )


class AlumnoFilter(FilterWithSelectRelated):
    list_select_related = ('estudiante__profile', )


class ItemServicioAdmin(AdminWithSelectRelated):
    list_display = ('nombre', 'alumno', 'monto_pagado', 'comprobante', )
    list_filter = (
        'alumno_servicio__alumno__seccion__grado',
        ('alumno_servicio__alumno', AlumnoFilter),
    )

    list_select_related = (
        'comprobante__forma_pago',
        'alumno_servicio__alumno__estudiante__profile',
        'alumno_servicio__alumno__seccion__grado',
        'alumno_servicio__servicio__grado',
    )


class ItemBazarAdmin(AdminWithSelectRelated):
    list_display = ('nombre', 'alumno', 'monto_pagado', 'comprobante', )
    list_filter = (
        'alumno_item__alumno__seccion__grado',
        ('alumno_item__alumno', AlumnoFilter),
    )

    list_select_related = (
        'comprobante__forma_pago',
        'alumno_item__alumno__estudiante__profile',
        'alumno_item__alumno__seccion__grado',
        'alumno_item__item__anio_lectivo',
    )


admin.site.register(FormaPago)
admin.site.register(Comprobante, ComprobanteAdmin)
admin.site.register(ItemServicio, ItemServicioAdmin)
admin.site.register(ItemBazar, ItemBazarAdmin)

我所要做的就是定义 select_related 字段,自定义 AdminWithSelectRelatedAdminInlineWithSelectRelatedFilterWithSelectRelated 将它们用于更改列表、更改表单,甚至内联表单集。

像魅力一样工作。

【讨论】:

似乎 django 现在开箱即用地支持这一点,因此您可以将 list_select_related 添加到您的管理类中,而无需使用 AdminBaseWithSelectRelated 之类的东西。见docs.djangoproject.com/en/2.2/ref/contrib/admin/… 嗨@MatthijsKooijman。 select_related 为 AdminClass 工作了很长时间。我的解决方案适用于内联和过滤器。 啊。我在想您的解决方案也适用于普通管理员,因为您是从 BaseAdmin 派生的。我还假设内置的 selectRelated 支持实际上也适用于内联,因为我认为它也来自 BaseAdmin?不过还没有测试。【参考方案3】:

formset 解决方案确实对我有用,但方法略有不同:

class MyInlineFormset(BaseInlineFormSet):
    def __init__(self, *args, **kwargs):
        super(MyInlineFormset, self).__init__(*args, **kwargs)
        self.queryset = self.queryset.prefetch_related('priced_product__product')

BaseInlineFormSet 类为您过滤查询集,您需要获取过滤后的查询集并添加预取。使用您的表单集实现(all() 查询集),您将获得不相关的 ComingProduct 对象,并且渲染时间可能太长。当它是过滤后的查询集时,它会非常快速地呈现。

【讨论】:

哦。这是一个救生员。我有一个模型 A,它有许多模型 B 的内联。B 有三个 m2m 和两个 fk 关系。而且。使用了模型翻译。此外,Grappelli 的自动完成功能也提供了帮助。 怎么样? Django的相关代码好像没变 @ramusus: 尝试过formfield_for_foreignkey 及其与 django 2.1 的作品 适用于 Django 2.2【参考方案4】:

我目前正在处理类似的问题。我发现的内容记录在此线程中:Translatable Manytomany fields in admin generate many queries

我所做的一个重要观察是我的解决方案仅适用于 Django 1.7x 而不适用于 1.8。完全相同的代码,使用 d1.7 我有 10^1 个查询的顺序,而新安装 d1.8 我有 10^4 个。

【讨论】:

以上是关于Django 管理员内联:select_related的主要内容,如果未能解决你的问题,请参考以下文章

在 Django 管理员中嵌套内联?

Django Admin:访问内联管理员中的父实例

Django 管理员内联:select_related

Django 中的内联表单验证

多对多字段的 Django 1.2.1 内联管理

Django 管理员自定义验证 - 至少需要一个内联外键模型