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

时间:2015-12-17 01:17:10

标签: json django orm

我正在使用Django 1.9及其内置的JSONField和Postgres 9.4。 在我的模型的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来完成,但我还没有研究过它。

6 个答案:

答案 0 :(得分:27)

从django 1.11(尚未发布,因此可能会发生变化),您可以使用django.contrib.postgres.fields.jsonb.KeyTextTransform代替RawSQL

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

#>>

您甚至可以在Model.objects.annotate( val=KeyTextTransform('json_field_key', 'blah__json_field')) ).aggregate(min=Min('val') 中添加KeyTextTransform进行全文搜索

SearchVector

请记住,您也可以在jsonb字段中编制索引,因此您应该根据您的特定工作量考虑这一点。

答案 1 :(得分:17)

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

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'一样(我的意思是单引号)。因此,如果您对此名称进行硬编码,则应记住插入它们,否则您将收到错误。

///有点offtopic ///

还有一个棘手的问题与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这样充满活力的解决方案。

答案 2 :(得分:5)

我知道这有点晚了(几个月),但我试图这样做时遇到了这个帖子。通过以下方式管理:

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'是我通过注释创建的变量的名称。

希望这有助于某人!

答案 3 :(得分:1)

可以使用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发生了很多事情,可能会给您手写实现带来麻烦。

答案 4 :(得分:0)

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

我这样工作:

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

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

答案 5 :(得分:0)

自 Django 3.1 起,JSON 字段上的 KeyTextTransform 函数可以运行 for all database backends。它映射到 ->> operator in Postgres

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

首先将存储的 JSON 的 my_keyattrs 属性注释到查询集(在本例中为整数字段。需要 Cast 以与所有数据库后端保持兼容):< /p>

from django.db.models import IntegerField
from django.db.models.fields.json import KeyTextTransform

qs = Model.objects.annotate(
    my_key=Cast(
        KeyTextTransform("my_key", "attrs"), models.IntegerField()
    )

现在您可以根据自己的喜好进行聚合:

from django.db.models import Min, Max, Avg, IntegerField
from django.db.models.functions import Cast, Round

qs.aggregate(
    min_value=Round(Min("my_key")),
    max_value=Round(Max("my_key")),
    avg_value=Cast(Round(Avg("my_key")), IntegerField()),
)

>>> {'min_value': 1, 'max_value' 10:, 'avg_value': 6}