7

我有一堆 django 模型

class ReviewItem(Model):
  review = models.ForegnKey("Review")
  person = models.ForeignKey("Person")
  category = models.ForeignKey("Category")
  item = models.ForeignKey("item")
  reviewed = models.DateTimeField(null=True)

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

class Category(Model):
  name = models.CharField(max_length=127)

class Item(Model):
  name = models.CharField(max_length=127)
  category = models.ForeignKey("Category")

(如您所见,ReviewItem 中的 "Category" fk 是多余的)

最多会有 NxM 条 ReviewItem 记录,其中 N 是人数,M 是他们可能分配给他们的项目数,并且在他们被审查后将设置他们的“审查”日期。项目按类别分组。

我想要的是计算每个项目有多少项目已经被审查,有多少没有。在 SQL 中,我可以做

select category.name, item.name,
sum(case when reviewed is null then 1 else 0 end) as un_reviewed
sum(case when reviewed is null then 0 else 1 end) as reviewed
from reviewitem
join  category on category.id = reviewitem.category_id
join  item on item.id = reviewitem.item_id
group by category.id, item.id
order by category.name, item.name

如果不在 django 中执行两个单独的 QuerySet,我无法弄清楚如何做到这一点。

使用两个 QuerySet 进行操作,我最终得到:

uncompleted_items = Item.objects.filter(
    reviewitem__review=current_review,
    reviewitem__person__reports_to=eff_user,
    reviewitem__reviewed__isnull=True
).select_related(
    'category',
).annotate(num_uncompleted=Count('reviewitem'))

completed_items = Item.objects.filter(
    reviewitem__review=current_review,
    reviewitem__person__reports_to=eff_user,
    reviewitem__reviewed__isnull=False
).select_related(
    'category',
).annotate(num_completed=Count('reviewitem'))
4

4 回答 4

10

在 Django 1.8 中,这可以使用Conditional Expressions来完成。

因此,您的示例查询可能如下所示:

from django.db.models import When, Case, Sum, IntegerField

items = Item.objects.annotate(
    un_reviewed=Sum(Case(When(reviewed__isnull=True, then=1)
                         When(reviewed__isnull=False, then=0),
                         output_field=IntegerField())),
    reviewed=Sum(Case(When(reviewed__isnull=True, then=0)
                      When(reviewed__isnull=False, then=1),
                      output_field=IntegerField())))
于 2015-10-13T07:05:35.283 回答
5

虽然这样做不太方便,但使用 Django ORM 并非不可能做到这一点:)

好的,hacky 解决方案不起作用,所以这里是漂亮的解决方案;)

from django.db import models
from test_models.models import ReviewItem


class CountNullSql(models.sql.aggregates.Count):
    sql_template = '%(function)s(%(distinct)s%(field)s IS NULL)'


class CountNotNullSql(CountNullSql):
    sql_template = '%(function)s(%(distinct)s%(field)s IS NOT NULL)'


class CountNull(models.Count):
    sql = CountNullSql

    def add_to_query(self, query, alias, col, source, is_summary):
        aggregate = self.sql(
            col,
            source=source,
            is_summary=is_summary,
            **self.extra)
        query.aggregates[alias] = aggregate

    def _default_alias(self):
        return '%s__%s' % (self.lookup, self.sql.__class__.__name__.lower())

    default_alias = property(_default_alias)


class CountNotNull(CountNull):
    sql = CountNotNullSql


items = (ReviewItem.objects
    .values(
        'item__category__name',
        'item__name',
    ).annotate(
        unreviewed=CountNull('reviewed'),
        reviewed=CountNotNull('reviewed'),
    )
)


# Just debug stuff from here on, might be useful for others :)    
sql, params = items.query.sql_with_params()

try:
    import sqlparse
    sql = sqlparse.format(sql, reindent=True)
except ImportError:
    pass

try:
    from pygments import highlight
    from pygments.lexers import SqlLexer
    from pygments.formatters import Terminal256Formatter
    sql = highlight(sql, SqlLexer(), Terminal256Formatter(style='colorful'))
except ImportError:
    pass

print sql

结果查询:

SELECT "test_models_category"."name",
       "test_models_item"."name",
       COUNT("test_models_reviewitem"."reviewed" IS NULL) AS "unreviewed",
       COUNT("test_models_reviewitem"."reviewed" IS NOT NULL) AS "reviewed"
FROM "test_models_reviewitem"
INNER JOIN "test_models_item" ON ("test_models_reviewitem"."item_id" = "test_models_item"."id")
INNER JOIN "test_models_category" ON ("test_models_item"."category_id" = "test_models_category"."id")
GROUP BY "test_models_category"."name",
         "test_models_item"."name"

示例结果:

[{'item__category__name': u'cat a',
  'item__name': u'aa',
  'reviewed': 1,
  'unreviewed': 1},
 {'item__category__name': u'cat a',
  'item__name': u'ab',
  'reviewed': 1,
  'unreviewed': 1},
 {'item__category__name': u'cat b',
  'item__name': u'ba',
  'reviewed': 1,
  'unreviewed': 1},
 {'item__category__name': u'cat b',
  'item__name': u'bb',
  'reviewed': 1,
  'unreviewed': 1}]

来自 Paul Tomblin 的更新 所描述的 CountNull 和 CountNotNull 方法不起作用(显然布尔值无论是真还是假都算作 1),所以我将它们更改如下:

from django.db import models


class CountNullSql(models.sql.aggregates.Sum):
    sql_template = '%(function)s((%(field)s IS NULL)::integer)'


class CountNotNullSql(CountNullSql):
    sql_template = '%(function)s((%(field)s IS NOT NULL)::integer)'


class CountNull(models.Sum):
    sql = CountNullSql

    def add_to_query(self, query, alias, col, source, is_summary):
        aggregate = self.sql(
            col,
            source=source,
            is_summary=is_summary,
            **self.extra)
        query.aggregates[alias] = aggregate

    def _default_alias(self):
        return '%s__%s' % (self.lookup, self.sql.__class__.__name__.lower())

    default_alias = property(_default_alias)


class CountNotNull(CountNull):
    sql = CountNotNullSql
于 2013-06-30T23:21:06.317 回答
3

这可以通过 Django 1.8 中的内置支持直接完成。请参阅https://docs.djangoproject.com/en/dev/ref/models/conditional-expressions/

于 2015-04-01T05:53:14.757 回答
1

尝试这个:

params = {'ri_table': ReviewItem._meta.db_table, 'i_table': Item._meta.db_table}
reviewed_query = 'SELECT COUNT(*) FROM %(ri_table)s WHERE %(ri_table)s.item_id=%(i_table)s.id AND reviewed IS NOT NULL' % params
unreviewed_query = 'SELECT COUNT(*) FROM %(ri_table)s WHERE %(ri_table)s.item_id=%(i_table)s.id AND reviewed IS NULL' % params
items = Item.objects.extra(select={'reviewed': reviewed_query, 'unreviewed': unreviewed_query})

这将返回一个项目的查询集,这些项目本质上用评论和取消评论的计数进行了注释。每个项目都是一个模型对象,因此您可以从那里获取类别等。print items.query将向您展示 SQL 语句,但当然,在构建模型实例对象时会发生更多的 Django 魔术。

使用 Django ORM 在单个查询中收集所有这些数据并不优雅(公平地说,它也是不平凡的 SQL)。它足够复杂,我认为不值得这样做,至少最初不值得。Django ORM 范式中更简单的代码将更容易理解、编码更快且更易于维护。它可能(或可能不)需要对数据库进行更多的点击,但这种担心可能是过早的优化。诸如模式规范化之类的事情可能会为您带来更大的性能提升。根据您的使用情况,像这样获取计数实际上可能会更快。

reviewed = item_obj.reviewitem_set.exclude(reviewed__isnull=True).count()
unreviewed = item_obj.reviewitem_set.filter(reviewed__isnull=True).count()
于 2013-07-01T22:05:13.533 回答