如何在 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')
您甚至可以在SearchVector
s 中包含KeyTextTransform
s 以进行全文搜索
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.1KeyTextTransform
可以通过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 中搜索带有空格的键?