Как агрегировать (мин/макс и т.д.) Данные Django JSONField?

Я использую 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 выражений запросов/поисков, но я еще не изучал его.

Ответ 1

Для тех, кто интересуется, я нашел решение (или обходное решение как минимум).

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. В этом случае вы получите DataError из postgres в результате выполнения вышеуказанного запроса. Значения 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

Из 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')

Вы можете включить KeyTextTransform в SearchVector для полнотекстового поиска

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

Помните, что вы также можете индексировать в jsonb-полях, поэтому вы должны учитывать это на основе вашей конкретной рабочей нагрузки.

Ответ 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)

где "данные" - это свойство jsonb, а "numeric_val" - это имя переменной, которую я создаю путем аннотации.

Надеюсь, это поможет кому-то!

Ответ 4

Кажется, нет никакого родного способа сделать это.

Я работал так:

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

Это можно сделать с помощью функции 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 происходит так много всего, что может укусить вас от написанных от руки реализаций.