Джанго предварительной выборки, связанные с фильтром на максимальное значение


У нас есть пара моделей, которые выглядят (примерно) так:

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

Всякий раз, когда мы загружаем Machines из базы данных, мы всегда в конечном итоге используем latest_update для этой машины. Когда мы впервые реализовали это, у нас было много машин и довольно небольшое количество обновлений на каждую машину, поэтому для повышения производительности (за счет уменьшения количества запросов) мы добавили в диспетчер моделей простую предварительную выборку по умолчанию для Machine:

class MachineManager(models.Manager):

    def get_queryset(self):
        return super(MachineManager, self).get_queryset().prefetch_related('machineupdate_set')

Однако все изменилось, и теперь у нас есть огромное количество обновлений связанные с каждой машиной, и запрос предварительной выборки начинает становиться проблемой (как с точки зрения длительного времени выполнения запроса, так и потребления памяти).

Мы ищем более разумный способ предварительной выборки требуемых данных, поскольку все, что нам действительно нужно для предварительной выборки,-это последнее обновление для каждой машины, а не все из них. Посмотрев на Django prefetch_related docs , мы могли бы изменить 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)

А затем измените latest_update, чтобы использовать новый атрибут, заполненный предварительной выборкой. Однако это не работает, потому что всякий раз, когда мы фильтруем запрос Machine, Используя это, мы получаем ошибку: AssertionError: Cannot filter a query once a slice has been taken.

Может ли кто-нибудь предложить решение этой проблемы, чтобы мы могли эффективно загрузить latest_update для каждой машины? Мы не уверены, как исправить проблему, которую мы имеем с вышеупомянутой попыткой предварительной выборки последних обновлений.

(FYI-мы рассматривали добавление is_latest_update логического поля к MachineUpdate, которое мы можем фильтровать, или в качестве альтернативы ссылка на внешний ключ latest_update на Machine, однако мы хотим избежать необходимости поддерживать эту избыточную информацию).

1 3

1 ответ:

Я вижу, что MachineUpdate.update_time имеет auto_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)

Использование:

machines = Machine.objects.filter(...).with_last_machineupdate()

Если это не так, например, мы не можем использовать Max('machineupdate__id'), и нам нужно придерживаться поля update_time. Тогда немного более оптимизированное решение (но все еще получающее все MachineUpdates за 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