【问题标题】:How to aggregate (min/max etc.) over Django JSONField data?如何在 Django JSONField 数据上聚合(最小/最大等)?
【发布时间】:2016-03-23 08:39:57
【问题描述】:

我正在使用带有内置 JSONField 和 Postgres 9.4 的 Django 1.9。 在我的模型的attrs json 字段中,我存储了带有一些值的对象,包括数字。我需要汇总它们以找到最小/最大值。 像这样的:

Model.objects.aggregate(min=Min('attrs__my_key'))

另外,提取特定的键也很有用:

Model.objects.values_list('attrs__my_key', flat=True)

上述查询失败

FieldError:“无法将关键字 'my_key' 解析为字段。不允许加入 'attrs'。”

有可能吗?

注意事项:

  1. 我知道如何使用简单的 Postgres 查询来完成这项工作,但我正在专门寻找一种 ORM 解决方案以具有过滤等功能。
  2. 我想这可以通过(相对)新的查询表达式/查找 API 来完成,但我还没有研究过。

【问题讨论】:

  • 下面的答案很好。可以找到另一个讨论这个问题的有用网站here

标签: json django orm


【解决方案1】:

从 django 1.11(还没有发布,所以这可能会改变)你可以使用 django.contrib.postgres.fields.jsonb.KeyTextTransform 而不是 RawSQL

在 django 1.10 中,您必须将 KeyTransform 复制/粘贴到您自己的 KeyTextTransform 并将 -> 运算符替换为 ->>#>#>> 以便它返回文本而不是 json 对象。

Model.objects.annotate(
    val=KeyTextTransform('json_field_key', 'blah__json_field'))
).aggregate(min=Min('val')

您甚至可以在SearchVectors 中包含KeyTextTransforms 以进行全文搜索

Model.objects.annotate(
    search=SearchVector(
        KeyTextTransform('jsonb_text_field_key', 'json_field'))
    )
).filter(search='stuff I am searching for')

请记住,您也可以在 jsonb 字段中建立索引,因此您应该根据您的具体工作量来考虑。

【讨论】:

  • 谢谢。我花了一段时间来解码如何使用它。在我的例子中,json数据存储在模型字段“jdata”(相当于问题中的“attrs”)中,json键是“createdDate”,它是顶级的。 min_result = Model.objects.annotate(val=KeyTextTransform('createdDate','jdata')).aggregate(min=Min('val'))
  • 请注意:由于django 3.1 KeyTextTransform可以通过from django.db.models.fields.json import KeyTextTransform导入
【解决方案2】:

对于那些感兴趣的人,我已经找到了解决方案(或至少解决方法)。

from django.db.models.expressions import RawSQL

Model.objects.annotate(
    val=RawSQL("((attrs->>%s)::numeric)", (json_field_key,))
).aggregate(min=Min('val')

请注意,attrs->>%s 表达式在处理后会变得像attrs->>'width' 一样(我的意思是单引号)。所以如果你硬编码这个名字,你应该记得插入它们,否则你会出错。

/// 有点离题 ///

还有一个与 django 本身无关但需要以某种方式处理的棘手问题。由于attrs 是 json 字段,并且对其键和值没有限制,您可以(取决于您的应用程序逻辑)在例如width 键中获取一些非数字值。在这种情况下,您将从 postgres 获得 DataError 作为执行上述查询的结果。 NULL 值将同时被忽略,所以没关系。如果你能抓住错误,那么没问题,你很幸运。在我的情况下,我需要忽略错误的值,这里唯一的方法是编写自定义 postgres 函数来抑制转换错误。

create or replace function safe_cast_to_numeric(text) returns numeric as $$
begin
    return cast($1 as numeric);
exception
    when invalid_text_representation then
        return null;
end;
$$ language plpgsql immutable;

然后使用它将文本转换为数字:

Model.objects.annotate(
    val=RawSQL("safe_cast_to_numeric(attrs->>%s)", (json_field_key,))
).aggregate(min=Min('val')

因此,对于像 json 这样的动态事物,我们得到了相当可靠的解决方案。

【讨论】:

  • django 文档并没有真正解释您正在编写的 sql 是如何工作的。在文档中,他们明确命名您从中选择的表。是否有任何其他文档详细说明了您可以省略和不能省略的内容?
  • 我发现如果字段名称是驼峰式大小写,则必须包含转义的双引号。我还发现 ::numeric 失败但 cast( ... as numeric ) 有效。示例 ... _annotations = { '_cashTotal':RawSQL("cast(\"payinJson\"->>%s as numeric)",("cashTotal",)), '_driverFuel':RawSQL("cast(\" payinJson\"->>%s as numeric)",("driverFuel",)), '_fuelAmount':RawSQL("cast(\"payinJson\"->>%s as numeric)",("fuelAmount", )) }
【解决方案3】:

我知道这有点晚了(几个月),但我在尝试这样做时遇到了这个帖子。设法做到这一点:

1) 使用 KeyTextTransform 将 jsonb 值转换为文本

2) 使用 Cast 将其转换为整数,以便 SUM 起作用:

q = myModel.objects.filter(type=9) \
.annotate(numeric_val=Cast(KeyTextTransform(sum_field, 'data'), IntegerField()))  \
.aggregate(Sum('numeric_val'))

print(q)

其中 'data' 是 jsonb 属性,'numeric_val' 是我通过注释创建的变量的名称。

希望这对某人有所帮助!

【讨论】:

  • 更正我的帖子!看起来您需要做一个额外的第一步,将注释添加到 cast 中。 ` q = myModel.objects.filter(type=8) \ .annotate(data_number=KeyTextTransform(sum_field, 'data')) \ .annotate(numeric_val=Cast('data_number', IntegerField())) \ .aggregate(Sum ('numeric_val')) `
  • 如果您想更正,您可以编辑自己的答案。
【解决方案4】:

使用 Postgres 函数可以做到这一点

https://www.postgresql.org/docs/9.5/functions-json.html

from django.db.models import Func, F, FloatField
from django.db.models.expressions import Value
from django.db.models.functions import Cast

text = Func(F(json_field), Value(json_key), function='jsonb_extract_path_text')
floatfield = Cast(text, FloatField())

Model.objects.aggregate(min=Min(floatfield))

这比使用RawQuery 好得多,因为如果您执行更复杂的查询,它不会中断,其中 Django 使用别名并且存在字段名称冲突。 ORM 有很多事情可以用手写的实现来咬你。

【讨论】:

    【解决方案5】:

    似乎没有本地方法可以做到这一点。

    我是这样工作的:

    my_queryset = Product.objects.all() # Or .filter()...
    max_val = max(o.my_json_field.get(my_attrib, '') for o in my_queryset)
    

    这远非奇妙,因为它是在 Python 级别(而不是 SQL 级别)完成的。

    【讨论】:

      【解决方案6】:
      from django.db.models.functions import Cast
      from django.db.models import Max, Min
      
      qs = Model.objects.annotate(
          val=Cast('attrs__key', FloatField())
      ).aggregate(
          min=Min("val"),
          max=Max("val")
      )
      

      【讨论】:

      • 在此代码中包含对答案的解释可能会很好,特别是因为这里已经有其他答案。这个答案有何不同?
      【解决方案7】:

      从 Django 3.1 开始,JSON 字段上的 KeyTextTransform 函数可以工作 for all database backends。它映射到->> operator in Postgres

      它可用于在聚合之前在查询集结果上注释 JSONField 内的特定 JSON 值。一个更清楚的例子如何利用它:

      首先,我们需要对要聚合的键进行注释。因此,如果您有一个 Django 模型,其 JSONField 名为 data 并且包含的​​ JSON 如下所示:

      {
          "age": 43,
          "name" "John"
      }
      

      您可以将查询集注释如下:

      from django.db.models import IntegerField
      from django.db.models.fields.json import KeyTextTransform
      
      qs = Model.objects.annotate(
          age=Cast(
              KeyTextTransform("age", "data"), models.IntegerField()
          )
      

      Cast 需要与所有数据库后端保持兼容。

      现在您可以根据自己的喜好进行汇总:

      from django.db.models import Min, Max, Avg, IntegerField
      from django.db.models.functions import Cast, Round
      
      qs.aggregate(
          min_age=Round(Min("age")),
          max_age=Round(Max("age")),
          avg_age=Cast(Round(Avg("age")), IntegerField()),
      )
      
      >>> {'min_age': 25, 'max_age' 82:, 'avg_age': 33}
      

      【讨论】:

      • 您能否提供“my_key”和“attrs”的示例值?
      • @JonathanAplacador 如果您有一个带有名为data 的JSONField 的django 模型,并且在此数据中您有一个键age,它将是:age=Cast(KeyTextTransform("age", "data"), models.IntegerField()
      猜你喜欢
      • 1970-01-01
      • 2015-09-25
      • 1970-01-01
      • 2023-04-03
      • 1970-01-01
      • 2017-08-31
      • 2021-01-04
      • 1970-01-01
      • 2018-11-11
      相关资源
      最近更新 更多