2015-03-02 1 views
3

У нас есть пара моделей, которые выглядят (примерно), как это:Джанго Prefetch Связанные с фильтром на максимальное значение

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, однако мы хотим, чтобы избежать необходимости поддерживать эту избыточную информацию).

ответ

3

Я вижу, что имеет 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 
+0

Это работало сказочно - мы на самом деле в конечном итоге реализации сочетание обоих вы будете предложения, используя 'last_machineupdate_list' в объектах возвращаются определенными запросами менеджера и возвращаются к более эффективному подходу, предложенному вами в конце ответа. – robjohncox

+1

Как отметил мой коллега, мы также смогли поместить 'objects = MachineQueryset.as_manager()' в класс 'Machine', устраняя необходимость в пустом классе MachineManager. – robjohncox

+0

Благодарим вас за отправку этого подробного примера! У меня есть аналогичная проблема, а также подумал об использовании 'annotate' как эффективного подхода, чтобы избежать множества небольших запросов.Ваше сообщение подтверждает, что это также может быть разумным решением для моего дела. – goetz