Django аннотирует запрос с подсчетом на подзапрос

Это не работает в django 1.1 (я считаю, что для этого потребуется подзапрос, поэтому появляется название)

qs.annotate(interest_level= \
             Count(Q(tags__favoritedtag_set__user=request.user))
           )

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

Есть ли способ построить такой запрос, не используя extra()?

Спасибо.

Ответы

Ответ 1

Рассматривая функцию add_aggregate в django/db/models/sql/query.py, объекты запроса не будут приниматься в качестве входных значений.

К сожалению, в настоящее время нет прямого пути в Django для агрегирования/аннотации того, что составляет запрос, особенно не тот, который дополнительно фильтруется каким-то образом.

Предполагая следующие модели:

class Item(models.Model):
    name = models.CharField(max_length=32)

class Tag(models.Model):
    itemfk = models.ForeignKey(Item, related_name='tags')
    name = models.CharField(max_length=32)

class FavoritedTag(models.Model):
    user = models.ForeignKey(User)
    tag = models.ForeignKey(Tag)

Кроме того, вы не можете аннотировать набор запросов в полях, определенных через .extra().

В SQL views.py можно было бы добавить такой код:

from testing.models import Item, Tag, FavoritedTag
from django.shortcuts import render_to_response
from django.contrib.auth.decorators import login_required
from django.utils.datastructures import SortedDict

@login_required
def interest_level(request):
    ruid = request.user.id

    qs = Item.objects.extra(
        select = SortedDict([
            ('interest_level', 'SELECT COUNT(*) FROM testing_favoritedtag, testing_tag \
            WHERE testing_favoritedtag.user_id = %s \
            AND testing_favoritedtag.tag_id = testing_tag.id \
            AND testing_tag.itemfk_id = testing_item.id'),
        ]),
        select_params = (str(ruid),)
    )

    return render_to_response('testing/interest_level.html', {'qs': qs})

Шаблон:

{% for item in qs %}
    name: {{ item.name }}, level: {{ item.interest_level }}<br>
{% endfor %}

Я тестировал это с помощью MySQL5. Поскольку я не эксперт по SQL, мне было бы любопытно, как оптимизировать здесь, или если есть другой способ "уменьшить" количество SQL. Может быть, есть какой-то интересный способ использования функции related_name здесь непосредственно в SQL?

Ответ 2

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

class Tag(models.Model):
    itemfk = models.ForeignKey(Item, related_name='tags')
    name = models.CharField(max_length=32)

    def get_favetag_count(self):
        """
        Calculate the number of times the current user has favorited a particular tag
        """

        favetag_count = FavoritedTag.objects.filter(tag=self,user=request.user).count()
        return favetag_count

Затем в шаблоне вы можете использовать что-то вроде:

{{tag}} ({{tag.get_favetag_count}})

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