Django 预取与最大值过滤相关

rob*_*cox 4 python django django-queryset database-performance

我们有一对看起来(大致)像这样的模型:

class Machine(models.Model):
    machine_id = models.CharField(max_length=10)
    # Other irrelevant fields

    @property
    def latest_update(self):
        if self.machineupdate_set.count() == 0:
            return None
        return self.machineupdate_set.order_by('-update_time')[:1].get()

class MachineUpdate(models.Model):
    machine = models.ForeignKey(Machine)
    update_time = models.DateTimeField(auto_now_add=True)
    # Other irrelevant fields
Run Code Online (Sandbox Code Playgroud)

Machine每当我们从数据库加载s 时,我们总是最终使用latest_update该机器的 。当我们第一次实现这一点时,我们有很多机器,每台机器的更新数量相当少,因此为了提高性能(通过减少查询计数),我们向模型管理器添加了一个简单的默认预取Machine

class MachineManager(models.Manager):

    def get_queryset(self):
        return super(MachineManager, self).get_queryset().prefetch_related('machineupdate_set')
Run Code Online (Sandbox Code Playgroud)

然而,情况发生了变化,现在我们有大量与每台机器相关的更新,预取查询开始成为一个问题(无论是查询执行时间长还是内存消耗)。

我们正在寻找一种更智能的方式来预取所需数据,因为我们真正需要预取的是每台机器的最新更新,而不是全部。查看了Django prefetch_lated 文档后,我们似乎可以将get_queryset其更改MachineManager为如下所示:

def get_queryset(self):
    latest_update_query = MachineUpdate.objects.order_by('-update_time')[:1]
    latest_update_prefetch = models.Prefetch('machineupdate_set', queryset=latest_update_query, to_attr='_latest_update')
    return super(MachineManager, self).get_queryset().prefetch_related(latest_update_prefetch)
Run Code Online (Sandbox Code Playgroud)

然后修改latest_update以使用预取填充的新属性。然而,这不起作用,因为每当我们Machine使用它过滤查询时,我们都会收到错误:AssertionError: Cannot filter a query once a slice has been taken.

谁能建议解决这个问题,以便我们可以有效地latest_update为每台机器加载?我们不确定如何解决上述尝试预取最新更新时遇到的问题。

(仅供参考 - 我们已经考虑添加一个可以过滤的is_latest_update布尔字段,或者添加一个外键引用,但是我们希望避免维护这些冗余信息)。MachineUpdatelatest_updateMachine

Tod*_*dor 5

我看到MachineUpdate.update_timeauto_now_add=True. 所以我们可以使用Max(MachineUpdate.id)每组Machine来获得最后一个MachineUpdate。正确的?如果是这样,True请检查以下代码:

class MachineManager(models.Manager):
    pass

class MachineQueryset(models.QuerySet):
    def with_last_machineupdate(self):
        return self.prefetch_related(models.Prefetch('machineupdate_set',
            queryset=MachineUpdate.objects.filter(
                id__in=Machine.objects \
                    .annotate(last_machineupdate_id=models.Max('machineupdate__id')) \
                    .values_list('last_machineupdate_id', flat=True) \
            ),
            #notice the list word
            to_attr='last_machineupdate_list'
        ))


class Machine(models.Model):
    machine_id = models.CharField(max_length=10)
    objects = MachineManager.from_queryset(MachineQueryset)()

    @property
    def latest_update(self):
        if hasattr(self, 'last_machineupdate_list') and len(self.last_machineupdate_list) > 0:
            return self.last_machineupdate_list[0]
        return None

class MachineUpdate(models.Model):
    machine = models.ForeignKey(Machine)
    update_time = models.DateTimeField(auto_now_add=True)

    def __unicode__(self):
        return str(self.update_time)
Run Code Online (Sandbox Code Playgroud)

用法:

machines = Machine.objects.filter(...).with_last_machineupdate()
Run Code Online (Sandbox Code Playgroud)

如果不是这种情况,例如我们不能使用Max('machineupdate__id'),我们需要坚持使用update_timefield。然后,稍微优化的解决方案(但仍然获得所有MachineUpdatesper Machine)如下所示:

class MachineManager(models.Manager):
    def get_queryset(self):
        return super(MachineManager, self).get_queryset() \
            .prefetch_related(models.Prefetch('machineupdate_set',
                queryset=MachineUpdate.objects.order_by('-update_time')
            ))

class Machine(models.Model):
    machine_id = models.CharField(max_length=10)
    objects = MachineManager()

    @property
    def latest_update(self):
        #this will not make queries
        machine_updates = self.machineupdate_set.all()
        if len(machine_updates) > 0:
            return machine_updates[0]
        return None
Run Code Online (Sandbox Code Playgroud)