Django фильтр JSONField, когда ключ является числом

node_status это JOSNField, как фильтровать node_status имеет {'2': True} из queryset.

>>> instance.node_status
{'cat': '1', '2': True, 'dog': True}
>>> qs.filter(node_status__cat=1)
Yeah got result
>>> qs.filter(node_status__has_key='dog')
Yeah got result
>>> qs.filter(node_status__2=True)
<QuerySet []>

node_status__2=True got empty queryset.

Django автоматически преобразует числоподобные значения в числа и индексы массивов:

Давайте сравним сырые SQLs

print(Foo.objects.filter(node_status__has_key='dog').query)
print(Foo.objects.filter(node_status__has_key='2').query)
SELECT `django4_foo`.`id`, `django4_foo`.`node_status` FROM `django4_foo` WHERE JSON_CONTAINS_PATH(`django4_foo`.`node_status`, 'one', $."dog")
SELECT `django4_foo`.`id`, `django4_foo`.`node_status` FROM `django4_foo` WHERE JSON_CONTAINS_PATH(`django4_foo`.`node_status`, 'one', $[2])

Как видите, 2 преобразуется в $[2], а dog преобразуется в $."dog"

Одним из возможных решений является получение необработанного SQL, переформатирование его вручную и инициализация нового набора запросов с использованием необработанного SQL:

raw_sql = str(Foo.objects.filter(node_status__has_key='2').query).replace(f'$[2]', f'\'$.2\'')

print(raw_sql)

rqs = Foo.objects.raw(raw_sql)

for o in rqs:
  print(o.node_status)
SELECT `django4_foo`.`id`, `django4_foo`.`node_status` FROM `django4_foo` WHERE JSON_CONTAINS_PATH(`django4_foo`.`node_status`, 'one', '$.2')
{'cat': '1', '2': True, 'dog': True}

Однако этот хак не работает с node_status__2=True фильтром.

Давайте проверим этот запрос на наличие dog ключа:

print(Foo.objects.filter(node_status__dog=True).query)

Выход:

SELECT `django4_foo`.`id`, `django4_foo`.`node_status` FROM `django4_foo` WHERE JSON_UNQUOTE(JSON_EXTRACT(`django4_foo`.`node_status`, $."dog")) = JSON_EXTRACT(true, '$')

Но фактически работающий сырой SQL это:

SELECT `django4_foo`.`id`, `django4_foo`.`node_status` FROM `django4_foo` WHERE JSON_UNQUOTE(JSON_EXTRACT(`django4_foo`.`node_status`, "$.dog")) = 'True'

Здесь много проблем с преобразованиями filterraw. ( сообщество Django также не рекомендует использовать str(query) для генерации сырых SQL запросов)

Django позиционируется как универсальная отвертка для всех типов винтов. Внутренние конфликты и переусложненная "универсальная и все в одном" логика - обычная проблема для Django.

Да, можно переопределить query класс и изменить логику преобразования ключей, но это сложно для чтения и понимания, довольно сложно и могут возникнуть проблемы с измененной логикой (потому что Django не ожидает, что логика была изменена).

Поэтому более простыми способами будут:

  • просто используйте Foo.objects.raw(sql_query), где sql_query предопределена вручную строка
  • использовать cursor.execute(sql_query, \[params\]) с sql_query предопределенной так же, как ①
  • .
  • получить Foo.objects.all() и использовать логику python для фильтрации ([e for e in Foo.objects.all() if '2' in e.node_status])

Другими возможными способами являются:

※ Мой выбор;)

Вернуться на верх