Как агрегировать (мин/макс и т.д.) Данные 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 "запрещено".
Возможно ли это как-то?
Заметки:
- Я знаю, как сделать простой запрос Postgres, чтобы выполнить эту работу, но я специально ищу решение ORM, чтобы иметь возможность фильтровать и т.д.
- Я полагаю, что это можно сделать с помощью (относительно) нового 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 происходит так много всего, что может укусить вас от написанных от руки реализаций.