Подтвердить что ты не робот

Django: как аннотировать запрос с количеством фильтрованных полей ForeignKey?

Вопрос о Django:

У меня есть следующие модели: каждый обзор предназначен для продукта, и каждый продукт имеет отдел:

class Department(models.Model):
    code = models.CharField(max_length=16)
class Product(models.Model):
    id = models.CharField(max_length=40, primary_key=True, db_index=True)
    dept = models.ForeignKey(Department, null=True, blank=True, db_index=True)
class Review(models.Model):
    review_id = models.CharField(max_length=32, primary_key=True, db_index=True) 
    product = models.ForeignKey(Product, db_index=True) 
    time = models.DateTimeField(db_index=True) 

Я хотел бы сделать запрос Django для диапазона дат (2012-01-01 - 2012-01-08) и вернуть список всех отделов, аннотированных идентификатором отдела и количеством продуктов из этого отдела которые были рассмотрены в течение этого диапазона дат.

Это немного жарит мой мозг:)

Я могу получить все отзывы за временной диапазон:

 reviews = Review.filter(time__range=["2012-01-01", "2012-01-08"])

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

В качестве альтернативы, лучше всего спросить отделы, а затем как-то аннотировать их с количеством продуктов?

4b9b3361

Ответ 1

Избегайте extra и raw, когда это возможно. агрегированные документы имеют почти этот прецедент:

Прямо из документов:

# Each publisher, each with a count of books as a "num_books" attribute.
>>> from django.db.models import Count
>>> pubs = Publisher.objects.annotate(num_books=Count('book'))
>>> pubs
[<Publisher BaloneyPress>, <Publisher SalamiPress>, ...]
>>> pubs[0].num_books
73

Итак, чтобы изменить это для вашего конкретного примера:

depts = Department.objects.
            filter(product__review__time__range=["2012-01-01", "2012-01-08"]).
            annotate(num_products=Count('product'))

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

Ответ 2

Мне нужно было сделать пару подобных запросов за последние несколько дней, и самый простой способ использовать extra queryset для аннотирования каждого объекта в вашем наборе запросов с фильтрованным количеством продуктов:

start = ..  # need to be formatted correctly
end = ...
departments = Departments.objects.all().extra(select = {
     'product_count' : """ SELECT COUNT(*) FROM appname_department
                           JOIN appname_product
                               ON appname_product.dept_id = appname_department.id
                           JOIN appname_review 
                               ON appname_review.product_id = appname_product.id
                           WHERE appname_review.time BETWEEN %s AND %s
                       """
}, params=[start, end])

и

{% for department in departments %}
    {{ department.product_count }}
{% endfor %}

Ответ 3

Документы для агрегации https://docs.djangoproject.com/en/dev/topics/db/aggregation/#cheat-sheet

Вероятно, есть способ использовать агрегат или аннотацию, но я предпочитаю это:

departments = Department.objects.all()
for dept in departments : 
    # Get the number of reviewed products for a given range and department
    num_products = dept.product_set.filter(review__time__range=["2012-01-01", "2012-01-08"]).count()

если вам это абсолютно необходимо как функция модели:

class Department(models.Model) :
    ...
    def num_products(self, start_date, end_date) : 
        return self.product_set.filter(review__time__range=[start_date, end_date]).count()

ИЗМЕНИТЬ

Я думаю, что если бы вы сделали необработанный запрос (что-то вроде этого)

sql = """SELECT COUNT(Product.*) as num_products, Department.*
    FROM Department
    LEFT OUTER JOIN Product ON Product.department = Department.id
    LEFT OUTER JOIN Review ON Product.id = Review.product
    WHERE Review.time BETWEEN "2012-01-01" AND "2012-01-08"
    GROUP BY Department.id"""

Department.objects.raw(sql)

а затем num_products будет атрибутом для каждого экземпляра Dept в результатах.

вам может понадобиться немного сыграть с именами полей + таблиц

Ответ 4

У меня такая же ситуация с аналогичной моделью данных.

Мое решение было похоже:

 Department.objects \
.extra(where=["<review_table_name.time_field> BETWEEN <time1> AND <time2> "])\
.annotate(num_products=Count('product__review__product_id'))