Джанго предварительной выборки, связанные с фильтром на максимальное значение
У нас есть пара моделей, которые выглядят (примерно) так:
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
Всякий раз, когда мы загружаем Machine
s из базы данных, мы всегда в конечном итоге используем 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 ответ:
Я вижу, что
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