如何在Django中预取相关对象?

时间:2018-10-15 08:54:16

标签: django django-models django-debug-toolbar

假设我具有以下模型以及相关方法:

class Turbine(models.Model):
    ...
    pass

def relContracts(self):
    contracts = self.contracted_turbines.all()
    return contracts

class Contract(models.Model):
    turbines = models.ManyToManyField(Turbine,related_name='contracted_turbines')

def _contracted_windfarm_name(self):
    windfarms = self.turbines.order_by().values_list("wind_farm__name", flat=True).distinct().select_related
    if len(windfarms) == 1:
        return windfarms[0]
    else:
        return ", ".join([str(x) for x in windfarms])
contracted_windfarm_name = property(_contracted_windfarm_name)

def _turbine_age(self):
    first_commisioning = self.turbines.all().aggregate(first=Min('commisioning'))['first']
    start = self.start_operation.year
    age = start - first_commisioning.year
    return age
turbine_age = property(_turbine_age)

Django-debug-toolbar告诉我,函数“ _contracted_windfarm_name”和“ _turbine_age”导致每个合同的数据库重复。

下面的get_queryset方法接收到我的合同查询集,其中我已经成功地为其他方法预取了“ turbines”:

def get_queryset(self, **kwargs):
    qs = super(ContractTableView, self).get_queryset().filter(active=True).prefetch_related('turbines', 'turbines__wind_farm')
    self.filter = self.filter_class(self.request.GET, queryset=qs)
    return self.filter.qs

我尝试预取“ turbines__contracted_turbines”,但不能减少重复数。

_contracted_windfarm_name方法用于按以下方式填充django-tables2方法的列:

contracted_windfarm = dt2.Column(accessor='contracted_windfarm_name', verbose_name='Wind Farm', orderable=False)

我在哪里误会?如何预取涡轮机的相关合同?

解决方案:第一个问题

我在get_queryset()方法中的queryset上添加了一个简单的注释:

def get_queryset(self, **kwargs):
    qs = super(ContractTableView, self).get_queryset()\
      .filter(active=True).prefetch_related('turbines', 'turbines__wind_farm')\
      .annotate(first_com_date=Case(When(turbines__commisioning__isnull=False, then=Min('turbines__commisioning'))))
    self.filter = self.filter_class(self.request.GET, queryset=qs)
    return self.filter.qs

这导致_turbine_age()方法略有变化:

def _turbine_age(self):
    first_commisioning = self.first_commisioning
    start = self.start_operation.year
    age = start - first_commisioning.year
    return age
turbine_age = property(_turbine_age)

解决方案:第二个问题

turbines__wind_farm方法中预取get_queryset()的情况下,无需调用distinct()方法:

def _contracted_windfarm_name(self):
    windfarms = list(set([str(x.wind_farm.name) for x in self.turbines.all()]))
    if len(windfarms) == 1:
        return windfarms[0]
    else:
        return ", ".join([str(x) for x in windfarms])
contracted_windfarm_name = property(_contracted_windfarm_name)

所有重复的查询都可以删除!

感谢@dirkgroten的宝贵贡献!

1 个答案:

答案 0 :(得分:0)

from django.db.models import Min

class ContractManager(models.Manager):
    def with_first_commissioning(self):
        return self.annotate(first_commissioning=Min('turbines__commissioning'))

class Contract(models.Model):
    objects = ContractManager()
    ...

然后,Contract.objects.with_first_commissioning()将为您返回一个查询集,其中包含每个first_commissioning的附加Contract值。因此,您可以在Contract._turbine_age()中删除第一行。

现在,风电场名称的情况要复杂一些。如果您使用的是Postgresql(支持StringAgg),则可以类似地在ContractManager中添加以下查询集:

from django.db.models import Subquery, OuterRef
from django.contrib.postgres.aggregates import StringAgg

def with_windfarms(self):
    wind_farms = WindFarm.objects.filter('turbines__contract'=OuterRef('pk')).order_by().distinct().values('turbines__contract')
    wind_farm_names = wind_farms.annotate(names=StringAgg('name', delimiter=', ')).values('names')
    return self.annotate(wind_farm_names=Subquery(wind_farm_names))

然后在您的_contracted_windfarm_name()方法中,假设您正在遍历查询集的结果,则可以访问self.wind_farm_names(如果您的方法在一种不同的方式。)

如果您不使用Postgresql,则只需更改查询集以执行hasattr,然后确保在此之后不添加任何与查询相关的逻辑:

prefetch_related

以便您可以在from django.db.models import Prefetch def with_windfarms(self): return self.prefetch_related(Prefetch('turbines', queryset=Turbine.objects.order_by().select_related('wind_farm').distinct('wind_farm__name'))) 方法中进行_contracted_wind_farms

在这两种情况下,我都假设您在视图中的某个位置通过查询集中的[str(x.wind_farm.name) for x in self.turbines]进行循环:

contracts