如何在 Django JSONField 数据上聚合(最小/最大等)?

Posted

技术标签:

【中文标题】如何在 Django JSONField 数据上聚合(最小/最大等)?【英文标题】:How to aggregate (min/max etc.) over Django JSONField data? 【发布时间】: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'。”

有可能吗?

注意事项:

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

【问题讨论】:

下面的答案很好。可以找到另一个讨论这个问题的有用网站here。 【参考方案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()

以上是关于如何在 Django JSONField 数据上聚合(最小/最大等)?的主要内容,如果未能解决你的问题,请参考以下文章

我应该如何从 bradjasper 的 django-jsonfield 升级到 Django 的内置 jsonfield?

如何在具有 Jsonfield 的模型中发布 django rest 中的数据

如何在 JSONField 中使用 Postgres 在 Django 中搜索带有空格的键?

django 无法访问模板中的 jsonfield 数据

Django:在过滤器和搜索中使用 JSONField 属性

如何在 DJANGO 中查询包含值列表的 JSONField