Django相当于sum + case

时间:2013-06-24 18:36:07

标签: django django-models

我有一堆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中的“类别”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 个答案:

答案 0 :(得分:6)

使用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())))

答案 1 :(得分:4)

尽管这样做不太方便,但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

答案 2 :(得分:2)

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

答案 3 :(得分: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})

这将返回一个Items的查询集,该查询集基本上使用评论和未查看的计数进行注释。每个Item都是一个模型对象,所以从那里你可以获得Category等等。 print items.query将向您显示SQL语句,但当然进一步的Django魔法会构建Model实例对象。

使用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()