2025, Dec 19 00:03
Почему вложенный dict в плейсхолдерах SQL не работает и как привязать JSON в Oracle
Почему %(nation[id])s и доступ к вложенному dict в плейсхолдерах SQL не работают. Как извлекать данные в Python и привязывать JSON для Oracle. Решение и пример
Когда вы передаёте в параметризованный SQL‑запрос словарь Python с вложенными данными, может возникнуть соблазн обратиться к этой структуре прямо из SQL‑строки. Частый приём — попытаться указать внутри плейсхолдера что‑то вроде person['country']['id'], рассчитывая, что драйвер БД сам разберёт путь. Вопрос в том, возможно ли это вообще и что делать вместо этого.
Пример проблемы
Шаблон ниже иллюстрирует попытку связать вложенное значение по пути прямо в SQL‑шаблоне:
stmt_tpl = ("""
INSERT INTO person
(id, name, country_id)
VALUES
(%(pid)s, %(full_name)s, %(nation[id])s)
""")
row_data = {'pid': 1, 'full_name': 'First Last', 'nation': {'id': 1, 'name': 'USA'}}
db_cur.execute(stmt_tpl, row_data)
Что происходит и почему
Идея проста: привязать плоские значения для id и name, а ещё — вложенный ключ nation['id'] как country_id. Подвох — в механике привязки. Плейсхолдеры подставляются по ключам из отображения, которое вы передаёте в execute. Обращение к вложенным структурам в духе %(nation[id])s, скорее всего, не поддерживается. Если нужен элемент из вложенного dict, сначала подготовьте корректное отображение в Python или извлеките нужные ключи до выполнения запроса. Иными словами, драйвер не обязан вычислять выражения индексирования словаря внутри SQL‑шаблона.
Решение с привязкой JSON в Oracle Database
Если вы используете Oracle Database, вложенную структуру можно хранить и привязывать как JSON. Так вы передаёте весь объект целиком, не пытаясь разыменовывать его части внутри SQL‑строки. В определении таблицы предусматривается столбец JSON, а при привязке в Python параметр помечается как JSON.
drop table if exists person;
create table person (id number, name varchar2(30), country_id json);
ins_text = """
INSERT INTO person
(id, name, country_id)
VALUES
(:id, :name, :country)
"""
payload = {'id': 1, 'name': 'First Last', 'country': {'id': 1, 'name': 'USA'}}
db_cur.setinputsizes(country=oracledb.DB_TYPE_JSON)
db_cur.execute(ins_text, payload)
Такой подход устраняет необходимость ссылаться на путь к вложенному ключу в SQL. Вместо этого вложенная структура привязывается одним JSON‑параметром.
Почему это важно
Важно понимать, где именно происходит преобразование данных. Попытка заставить SQL‑шаблон «пробираться» по вложенной структуре Python привносит хрупкую магию в текст запроса. Гораздо чище подготовить нужную форму данных в Python — либо выделить отдельные значения в плоское отображение, либо сохранять вложенную структуру как JSON, если это поддерживает ваша БД и модель. Стоит также вспомнить о моделировании. Нередко справедливо замечание, что это денормализованная схема: у country должна быть отдельная таблица, на которую ссылается person внешним ключом. И закономерно возникает вопрос, почему вообще используется вложенный словарь. Эти соображения помогут решить, нужно ли «расплющивать» данные или хранить их как JSON.
Вывод
Не рассчитывайте на синтаксис плейсхолдеров для доступа к вложенным dict — с высокой вероятностью это не сработает. Нужен скаляр? Сформируйте правильный словарь или извлеките ключи до вызова execute. Если вы работаете с Oracle Database и хотите сохранить вложенность, привязывайте структуру как JSON и используйте соответствующий JSON‑столбец. Логика преобразований в Python и простая привязка в SQL дают более ясный код и меньше неожиданностей.