【问题标题】:how to merge two annotated querysets into one result如何将两个带注释的查询集合并为一个结果
【发布时间】:2016-11-05 11:15:16
【问题描述】:

型号:

class Foo(models.model):
  name =  models.CharField(max_length = 50, blank = True, unique = True)

class Bar1(models.Model):
  foo = models.ForeignKey('Foo')
  value = models.DecimalField(max_digits=10,decimal_places=2)

class Bar2(models.Model):
  foo = models.ForeignKey('Foo')
  value = models.DecimalField(max_digits=10,decimal_places=2)

类 Bar1 和 Bar2 是不相关的,所以我不能把它作为一个类来解决问题。但这只是尽可能简单地展示问题的示例。

first = Foo.objects.all().annotate(Sum("bar1__value"))
second = Foo.objects.all().annotate(Sum("bar2__value"))

每个查询集都包含正确的值。

我无法将其合并到:

 both = Foo.objects.all().annotate(Sum("bar1__value")).annotate(Sum("bar2__value"))

因为总和值相乘 - 这是不幸的预期行为 - 因为 JOINS

现在的问题 - 如何合并/加入第一个和第二个以获得两者?

例子:

小节 1:

  foo | value
--------------
   A  |  10
   B  |  20
   B  |  20

小节 2:

  foo | value
--------------
   A  |  -0.10
   A  |  -0.10
   B  |  -0.25

两者(值不同取决于输入 bar1 和 bar2 的顺序)

  foo | bar1__value__sum | bar2__value__sum
---------------------------------
   A  |  20              | -0.20
   B  |  40              | -0.50

预期结果:

  foo | bar1__value__sum | bar2__value__sum
---------------------------------
   A  |  10              | -0.20
   B  |  40              | -0.25

我无法使用 itertools.chains,因为结果是:

  foo | bar1__value__sum | bar2__value__sum
---------------------------------
   A  |  null            | -0.20
   B  |  null            | -0.25
   A  |  10              | null
   B  |  40              | null

【问题讨论】:

  • 无法使用最新的 Django 1.10.3 重现
  • @madzohan 请立即检查更新的代码

标签: django


【解决方案1】:

您的问题是 Django 的 ORM 的一个已知限制:https://code.djangoproject.com/ticket/10060

如果您可以执行两个查询,这里有一个选项:

result = Foo.objects.annotate(b1_sum=Sum("bar1__value"))
bar2_sums = Foo.objects.annotate(b2_sum=Sum("bar2__value")).in_bulk()
for foo in result:
    foo.b2_sum = bar2_sums.get(foo.pk).b2_sum

【讨论】:

  • 执行两个查询没问题,但我不确定这将如何工作。 for 之后的结果与之前的结果相同。还是我错了?
  • 对不起,这个例子有几个错别字(现已修复)。这个想法是手动注释result 中的实例:当您再次迭代它时,您将从查询集缓存中获得相同的注释实例。除了 in_bulk,您还可以获取所需的 values() 并使用字典理解。
  • 是的,它有效,我查看了 djangoproject 的票并找到了其他解决方案 - 现在我正在尝试分析它。之后我选择更好的答案;D
  • 当我在视图的get_queryset 中使用它时,返回的result 没有附加b2_sum 字段。 (使用 Django 2.0.6)
【解决方案2】:

根据@emulbreh 的回答,我阅读了票证并找到了一些解决方案。我走这条路,做了这个:

models.py:

from django.db.models.expressions import RawSQL
from django.db.models.query import QuerySet
(...)
class NewManager(models.Manager):
  """A re-usable Manager to access a custom QuerySet"""
  def __getattr__(self, attr, *args):
    try:
      return getattr(self.__class__, attr, *args)
    except AttributeError:
    # don't delegate internal methods to the queryset
      if attr.startswith('__') and attr.endswith('__'):
        raise
      return getattr(self.get_query_set(), attr, *args)

  def get_query_set(self):
    return self.model.QuerySet(self.model, using=self._db)


class Foo(models.Model):
  name =  models.CharField(max_length = 50, blank = True, unique = True)
  objects =NewManager()
  def __str__(self):
    return self.name

  class QuerySet(QuerySet):
    def annotate_sum(self, modelClass, field_name):
      annotation_name="%s__%s__%s" % (modelClass._meta.model_name,field_name,'sum')
      raw_query = "SELECT SUM({field}) FROM {model2} WHERE {model2}.{model3}_id = {model1}.id".format(
              field = field_name,
              model3 = self.model._meta.model_name,
              model2 = modelClass._meta.db_table,
              model1 = self.model._meta.db_table
          )
      debug.debug("%s" % raw_query)
      annotation = {annotation_name: RawSQL(raw_query, [])}

      return self.annotate(**annotation)

还有views.py:

both = Foo.objects.annotate_sum(Bar1, 'value').annotate_sum( Bar2, 'value')

sql 结果正是我想要的:

SELECT "app_foo"."id", "app_foo"."name", (SELECT SUM(value) FROM app_bar1 WHERE app_bar1.foo_id = app_foo.id) AS "bar1__value__sum", (SELECT SUM(value) FROM app_bar2 WHERE app_bar2.foo_id = app_foo.id) AS "bar2__value__sum" FROM "app_foo"

当然它并不完美——它需要一些错误检查(例如双引号)或别名,但我认为这是正确的方向

【讨论】:

    【解决方案3】:

    我在遇到类似问题后登陆了此页面,但使用的是 Count 而不是 Sum

    最简单的解决方案是使用Count(<field>, distinct=True) 2号Count,即

    both = Foo.objects.all().annotate(Count("bar1__value")
                           ).annotate(Count("bar2__value", distinct=True))
    

    参考资料:

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-07-25
      • 1970-01-01
      • 2012-09-17
      • 1970-01-01
      • 1970-01-01
      • 2018-01-17
      • 1970-01-01
      相关资源
      最近更新 更多