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'
Здесь много проблем с преобразованиями filter → raw. ( сообщество 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])
Другими возможными способами являются:
- Интегрировать
sqlalchemy.
- Миграция на другой фреймворк (FastAPI, Flask и т.д.) ※
※ Мой выбор;)