Peewee ORM中CTE嵌套时JSON_TABLE解析逻辑丢失问题求助
问题
使用Peewee ORM创建CTE,通过原生SQL调用JSON_TABLE和JSON_KEYS解析字段中的JSON对象,将每个元素转为独立记录并关联其他表。单独执行包含该CTE的查询时,能生成包含完整JSON_TABLE解析逻辑的正确SQL;但将该CTE嵌套后关联其他表构建主查询时,JSON_TABLE的解析逻辑及WHERE条件完全丢失。
需解决:
- 确认问题是否与原生SQL的使用有关
- 如何让Peewee保留CTE的完整语法(包括原生SQL部分)
- 是否有其他实现JSON解析后关联多表的方案
目标SQL(可正常执行)
WITH solution AS ( SELECT DISTINCT ost_form_entry.object_id as ost_form_entry_object_id, jt.my_key, JSON_UNQUOTE(JSON_EXTRACT(ost_form_entry_values.value, CONCAT('$.', jt.my_key))) AS my_value, ost_ticket.number, ost_ticket.ticket_id, ost_ticket.created AS created_date, ost_ticket.closed AS closed_date FROM ost_form_entry_values JOIN ost_form_entry ON ost_form_entry_values.entry_id = ost_form_entry.id JOIN ost_ticket ON ost_form_entry.object_id = ost_ticket.ticket_id, JSON_TABLE( JSON_KEYS(ost_form_entry_values.value), '$[*]' COLUMNS ( my_key VARCHAR(100) PATH '$' ) ) AS jt WHERE JSON_VALID(ost_form_entry_values.value) AND JSON_TYPE(ost_form_entry_values.value) = 'OBJECT' AND ost_form_entry_values.field_id = 67 ), user_type AS ( SELECT DISTINCT jt.my_key, JSON_UNQUOTE(JSON_EXTRACT(ost_form_entry_values.value, CONCAT('$.', jt.my_key))) AS my_value, ost_form_entry.object_id as ost_form_entry_object_id FROM ost_form_entry_values JOIN ost_form_entry ON ost_form_entry_values.entry_id = ost_form_entry.id, JSON_TABLE( JSON_KEYS(ost_form_entry_values.value), '$[*]' COLUMNS ( my_key VARCHAR(100) PATH '$' ) ) AS jt WHERE JSON_VALID(ost_form_entry_values.value) AND JSON_TYPE(ost_form_entry_values.value) = 'OBJECT' AND ost_form_entry_values.field_id = 76 ), response AS ( SELECT DISTINCT CONVERT(ost_form_entry_values.value, DATETIME) AS my_value, ost_form_entry.object_id as ost_form_entry_object_id FROM ost_form_entry_values JOIN ost_form_entry ON ost_form_entry_values.entry_id = ost_form_entry.id WHERE ost_form_entry_values.value IS NOT NULL AND ost_form_entry_values.field_id = 75 ) SELECT DISTINCT solution.number AS ticket_no, solution.created_date, solution.closed_date, response.my_value AS first_response_date, solution.my_value AS solution, user_type.my_value AS user_type, solution.ticket_id FROM solution JOIN user_type ON solution.ost_form_entry_object_id = user_type.ost_form_entry_object_id JOIN response ON solution.ost_form_entry_object_id = response.ost_form_entry_object_id WHERE solution.closed_date BETWEEN '2024-05-22' AND '2024-08-26' ;
Peewee中构建的JSON解析CTE
cte = (OstFormEntryValues .select( OstFormEntryValues.entry_id, OstFormEntryValues.field_id, SQL('jt.my_key').alias(f'{field_name}_key'), fn.JSON_UNQUOTE(fn.JSON_EXTRACT(OstFormEntryValues.value, fn.CONCAT('$.', SQL('jt.my_key')))).alias(field_name) ) .from_(OstFormEntryValues, SQL('JSON_TABLE(JSON_KEYS(value), \'$[*]\' COLUMNS (my_key VARCHAR(100) PATH \'$\'))').alias('jt')) .where( fn.JSON_VALID(OstFormEntryValues.value) & (fn.JSON_TYPE(OstFormEntryValues.value) == 'OBJECT') & (OstFormEntryValues.field_id == field_id) ) .cte(field_name))
单独查询时生成的正确SQL(省略参数)
WITH `solution_found` AS ( SELECT `t1`.`entry_id`, `t1`.`field_id`, jt.my_key AS `solution_found_key`, JSON_UNQUOTE(JSON_EXTRACT(`t1`.`value`, CONCAT(%s, jt.my_key))) AS `solution_found` FROM `ost_form_entry_values` AS `t1`, JSON_TABLE(JSON_KEYS(value), '$[*]' COLUMNS (my_key VARCHAR(100) PATH '$')) AS `jt` WHERE ((JSON_VALID(`t1`.`value`) AND (JSON_TYPE(`t1`.`value`) = %s)) AND (`t1`.`field_id` = %s))) SELECT `t2`.`object_id`, `solution_found`.`entry_id`, `solution_found`.`field_id`, `solution_found`.`solution_found` FROM `ost_form_entry` AS `t2` INNER JOIN `solution_found` ON (`t2`.`id` = `solution_found`.`entry_id`)
嵌套CTE后主查询生成的错误SQL(丢失解析逻辑)
WITH `solution_found` AS ( SELECT `t1`.`object_id`, `solution_found`.`entry_id`, `solution_found`.`field_id`, `solution_found`.`solution_found` FROM `ost_form_entry` AS `t1` INNER JOIN `solution_found` ON (`t1`.`id` = `solution_found`.`entry_id`)) SELECT `t2`.`number`, `t2`.`ticket_id`, `solution_found`.`entry_id`, `solution_found`.`field_id`, `solution_found`.`solution_found` FROM `ost_ticket` AS `t2` INNER JOIN `solution_found` ON (`t2`.`ticket_id` = `solution_found`.`object_id`)
解决方案
问题原因
Peewee在处理CTE嵌套时,对原生SQL片段的跟踪存在缺陷。当将CTE作为子查询关联其他表时,Peewee没有正确保留CTE定义中的原生FROM子句(即JSON_TABLE部分)和WHERE条件,仅保留了CTE与其他表的关联逻辑,导致核心JSON解析逻辑丢失。
解决方法:显式定义完整CTE关联逻辑
不要分步构建CTE再关联,而是在CTE内部完成所有必要的表关联,确保Peewee能完整捕获整个CTE的SQL结构:
# 构建包含完整关联的solution CTE solution_cte = (OstFormEntryValues .select( OstFormEntry.object_id.alias('ost_form_entry_object_id'), SQL('jt.my_key'), fn.JSON_UNQUOTE(fn.JSON_EXTRACT(OstFormEntryValues.value, fn.CONCAT('$.', SQL('jt.my_key')))).alias('my_value'), OstTicket.number, OstTicket.ticket_id, OstTicket.created.alias('created_date'), OstTicket.closed.alias('closed_date') ) .join(OstFormEntry, on=(OstFormEntryValues.entry_id == OstFormEntry.id)) .join(OstTicket, on=(OstFormEntry.object_id == OstTicket.ticket_id)) .from_(OstFormEntryValues, SQL('JSON_TABLE(JSON_KEYS(value), \'$[*]\' COLUMNS (my_key VARCHAR(100) PATH \'$\'))').alias('jt')) .where( fn.JSON_VALID(OstFormEntryValues.value) & (fn.JSON_TYPE(OstFormEntryValues.value) == 'OBJECT') & (OstFormEntryValues.field_id == 67) ) .distinct() .cte('solution')) # 同理构建user_type CTE user_type_cte = (OstFormEntryValues .select( SQL('jt.my_key'), fn.JSON_UNQUOTE(fn.JSON_EXTRACT(OstFormEntryValues.value, fn.CONCAT('$.', SQL('jt.my_key')))).alias('my_value'), OstFormEntry.object_id.alias('ost_form_entry_object_id') ) .join(OstFormEntry, on=(OstFormEntryValues.entry_id == OstFormEntry.id)) .from_(OstFormEntryValues, SQL('JSON_TABLE(JSON_KEYS(value), \'$[*]\' COLUMNS (my_key VARCHAR(100) PATH \'$\'))').alias('jt')) .where( fn.JSON_VALID(OstFormEntryValues.value) & (fn.JSON_TYPE(OstFormEntryValues.value) == 'OBJECT') & (OstFormEntryValues.field_id == 76) ) .distinct() .cte('user_type')) # 同理构建response CTE response_cte = (OstFormEntryValues .select( fn.CONVERT(OstFormEntryValues.value, 'DATETIME').alias('my_value'), OstFormEntry.object_id.alias('ost_form_entry_object_id') ) .join(OstFormEntry, on=(OstFormEntryValues.entry_id == OstFormEntry.id)) .where( OstFormEntryValues.value.is_null(False) & (OstFormEntryValues.field_id == 75) ) .distinct() .cte('response')) # 主查询关联所有CTE main_query = (solution_cte .select( solution_cte.c.number.alias('ticket_no'), solution_cte.c.created_date, solution_cte.c.closed_date, response_cte.c.my_value.alias('first_response_date'), solution_cte.c.my_value.alias('solution'), user_type_cte.c.my_value.alias('user_type'), solution_cte.c.ticket_id ) .join(user_type_cte, on=(solution_cte.c.ost_form_entry_object_id == user_type_cte.c.ost_form_entry_object_id)) .join(response_cte, on=(solution_cte.c.ost_form_entry_object_id == response_cte.c.ost_form_entry_object_id)) .where( solution_cte.c.closed_date.between('2024-05-22', '2024-08-26') ) .distinct()) # 执行查询 results = main_query.execute()
这种方式将所有关联逻辑包含在CTE内部,Peewee会完整生成包含JSON_TABLE的CTE定义,不会丢失解析逻辑。
替代方案:自定义JSON_TABLE函数扩展
如果频繁使用JSON_TABLE,可以通过Peewee的Function类自定义该函数,让ORM更好地识别和跟踪它:
from peewee import Function class JSON_TABLE(Function): function = 'JSON_TABLE' # 使用自定义函数重构CTE cte = (OstFormEntryValues .select( OstFormEntryValues.entry_id, OstFormEntryValues.field_id, SQL('jt.my_key').alias(f'{field_name}_key'), fn.JSON_UNQUOTE(fn.JSON_EXTRACT(OstFormEntryValues.value, fn.CONCAT('$.', SQL('jt.my_key')))).alias(field_name) ) .from_( OstFormEntryValues, JSON_TABLE( fn.JSON_KEYS(OstFormEntryValues.value), SQL("'$[*]' COLUMNS (my_key VARCHAR(100) PATH '$')") ).alias('jt') ) .where( fn.JSON_VALID(OstFormEntryValues.value) & (fn.JSON_TYPE(OstFormEntryValues.value) == 'OBJECT') & (OstFormEntryValues.field_id == field_id) ) .cte(field_name))
自定义函数能让Peewee更准确地解析SQL结构,减少原生SQL片段被忽略的概率。
终极兜底:使用原生SQL字符串
如果上述方法都无效,可以直接使用Peewee的RawQuery执行完整的原生SQL语句,完全绕过ORM的SQL生成逻辑:
from peewee import RawQuery sql = """ WITH solution AS ( SELECT DISTINCT ost_form_entry.object_id as ost_form_entry_object_id, jt.my_key, JSON_UNQUOTE(JSON_EXTRACT(ost_form_entry_values.value, CONCAT('$.', jt.my_key))) AS my_value, ost_ticket.number, ost_ticket.ticket_id, ost_ticket.created AS created_date, ost_ticket.closed AS closed_date FROM ost_form_entry_values JOIN ost_form_entry ON ost_form_entry_values.entry_id = ost_form_entry.id JOIN ost_ticket ON ost_form_entry.object_id = ost_ticket.ticket_id, JSON_TABLE( JSON_KEYS(ost_form_entry_values.value), '$[*]' COLUMNS ( my_key VARCHAR(100) PATH '$' ) ) AS jt WHERE JSON_VALID(ost_form_entry_values.value) AND JSON_TYPE(ost_form_entry_values.value) = 'OBJECT' AND ost_form_entry_values.field_id = 67 ), user_type AS ( SELECT DISTINCT jt.my_key, JSON_UNQUOTE(JSON_EXTRACT(ost_form_entry_values.value, CONCAT('$.', jt.my_key))) AS my_value, ost_form_entry.object_id as ost_form_entry_object_id FROM ost_form_entry_values JOIN ost_form_entry ON ost_form_entry_values.entry_id = ost_form_entry.id, JSON_TABLE( JSON_KEYS(ost_form_entry_values.value), '$[*]' COLUMNS ( my_key VARCHAR(100) PATH '$' ) ) AS jt WHERE JSON_VALID(ost_form_entry_values.value) AND JSON_TYPE(ost_form_entry_values.value) = 'OBJECT' AND ost_form_entry_values.field_id = 76 ), response AS ( SELECT DISTINCT CONVERT(ost_form_entry_values.value, DATETIME) AS my_value, ost_form_entry.object_id as ost_form_entry_object_id FROM ost_form_entry_values JOIN ost_form_entry ON ost_form_entry_values.entry_id = ost_form_entry.id WHERE ost_form_entry_values.value IS NOT NULL AND ost_form_entry_values.field_id = 75 ) SELECT DISTINCT solution.number AS ticket_no, solution.created_date, solution.closed_date, response.my_value AS first_response_date, solution.my_value AS solution, user_type.my_value AS user_type, solution.ticket_id FROM solution JOIN user_type ON solution.ost_form_entry_object_id = user_type.ost_form_entry_object_id JOIN response ON solution.ost_form_entry_object_id = response.ost_form_entry_object_id WHERE solution.closed_date BETWEEN %s AND %s ; """ # 执行原生查询,传入参数 results = RawQuery(OstTicket, sql, ('2024-05-22', '2024-08-26')).execute()
这种方式完全避免ORM的SQL生成问题,适合复杂的JSON解析场景。
内容的提问来源于stack exchange,提问作者SpacemanSpiff
相关产品推荐
相关产品推荐

