2013-03-01 5 views
7

Предположим, я хочу показать список бегунов, упорядоченных по их последнему времени спринта.Django: заказ QuerySet на основе последнего детского модельного поля

class Runner(models.Model): 
    name = models.CharField(max_length=255) 

class Sprint(models.Model): 
    runner = models.ForeignKey(Runner) 
    time = models.PositiveIntegerField() 
    created = models.DateTimeField(auto_now_add=True) 

Это быстрый эскиз того, что я хотел бы сделать в SQL:

SELECT runner.id, runner.name, sprint.time 
FROM runner 
LEFT JOIN sprint ON (sprint.runner_id = runner.id) 
WHERE 
    sprint.id = (
    SELECT sprint_inner.id 
    FROM sprint as sprint_inner 
    WHERE sprint_inner.runner_id = runner.id 
    ORDER BY sprint_inner.created DESC 
    LIMIT 1 
) 
    OR sprint.id = NULL 
ORDER BY sprint.time ASC 

В Django QuerySet documentation состояний:

Допустимо указывать многозначное поле на заказ результаты (например, поле ManyToManyField). Обычно это не будет то, что нужно делать, и это действительно расширенная функция использования. Однако, если вы знаете, что фильтрация вашего запроса или доступные данные подразумевает, что для каждого из будет отображаться только один элемент данных заказа, то вы можете точно указать то, что вы хотите сделать. Используйте упорядочение по многозначным полям с осторожностью и убедитесь, что результаты ожидаются.

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

Одно примечание, потому что это не очевидно, в этом примере: таблица Runner будет иметь несколько сто записей, спринты также будут иметь несколько сотен, а через несколько дней, вероятно, несколько тысяч записей. Данные будут отображаться в разбитом на страницы, поэтому сортировка в Python не является вариантом.

Единственная другая возможность, которую я вижу, это написать SQL сам, но я бы хотел избежать этого любой ценой.

ответ

2

Я не думаю, что есть способ сделать это с помощью ОРМ только один запрос, вы можете захватить список бегунов и использовать annotate добавить свои последние Спринт идентификаторы - затем процеживают и упорядочивать эти спринты ,

>>> from django.db.models import Max 

# all runners now have a `last_race` attribute, 
# which is the `id` of the last sprint they ran 
>>> runners = Runner.objects.annotate(last_race=Max("sprint__id")) 

# a list of each runner's last sprint ordered by the the sprint's time, 
# we use `select_related` to limit lookup queries later on 
>>> results = Sprint.objects.filter(id__in=[runner.last_race for runner in runners]) 
...       .order_by("time") 
...       .select_related("runner") 

# grab the first result 
>>> first_result = results[0] 

# you can access the runner's details via `.runner`, e.g. `first_result.runner.name` 
>>> isinstance(first_result.runner, Runner) 
True 

# this should only ever execute 2 queries, no matter what you do with the results 
>>> from django.db import connection 
>>> len(connection.queries) 
2 

Это довольно быстро и будет по-прежнему использовать индексы баз данных и кэширование.

Несколько тысяч записей не так уж много, это должно хорошо работать для таких чисел. Если вы столкнулись с проблемами, я предлагаю вам укусить пулю и использовать необработанный SQL.

+0

Не связано ли это относительно высокое использование памяти? Насколько я могу судить, он забирает по крайней мере каждого бегуна в память и строит довольно большой список своих идентификаторов спринта. Выполнение этого на каждом просмотре страницы с несколькими сотнями участников в БД заставляет меня чувствовать себя * немного неудобно. Наверное, это кеширование. – Strayer

+1

После тестирования этого с 10 000 бегунов он использовал менее 10 МБ (3 МБ на самом деле ...) ОЗУ. Если вы думаете, что вам понадобится больше, вам действительно нужно использовать raw SQL. Как всегда, лучший подход к этому - сначала профиль, а не спекуляция. Преждевременная оптимизация и все это ... – Matt

+0

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

0
def view_name(request): 
    spr = Sprint.objects.values('runner', flat=True).order_by(-created).distinct() 
    runners = [] 
    for s in spr: 
     latest_sprint = Sprint.objects.filter(runner=s.runner).order_by(-created)[:1] 
     for latest in latest_sprint: 
      runners.append({'runner': s.runner, 'time': latest.time}) 

    return render(request, 'page.html', { 
      'runners': runners, 
    }) 


{% for runner in runners %} 
    {{runner.runner}} - {{runner.time}} 
{% endfor %} 
+0

Проблема заключается в том, чтобы не получить последний спринт, но заказывая Runner QuerySet своим последним полем «время» спринта. – Strayer

+0

Это действительно работает. Проблема заключается в том, что это перемещает порядок бегунов в приложение, что приводит к по меньшей мере большому использованию памяти и относительно высокому использованию ЦП. См. Обновленный вопрос о размерах таблиц. Другая проблема с этим подходом заключается в том, что он не покажет никаких бегунов, у которых вообще нет спринта. Хотя это также можно решить в коде python, это идеальная работа для базы данных, поскольку она может использовать свои индексы и кеши. Это работает для небольших баз данных, но наш SysAdmin убьет меня, если я сделаю это таким образом;) – Strayer

+0

Хммм ... это сложно. И мы такие же, я осторожен в кодировании, если о моей работе из-за ожидания моего работодателя. :) – catherine

Смежные вопросы