jsqlparser(六):TablesNamesFinder 深度解析与 SQL 格式化实现

在数据库应用开发中,SQL语句的解析和处理是一项常见而重要的任务。本文将深入探讨 JSQLParser 中的 TablesNamesFinder 类,分析其核心原理、与 AST 访问接口(CCJSqlParserVisitor )的关系、使用场景,并通过实际代码示例展示如何基于 TablesNamesFinder 实现 SQL 语句的格式化处理。

一、JSQLParser AST 访问机制概述

JSQLParser 采用访问者模式(Visitor Pattern)来处理解析后的 SQL 抽象语法树(AST)。在这个设计中,有几个核心组件:

1.1 核心访问接口

CCJSqlParserVisitor 是 JSQLParser 中定义的一个关键接口,它是 SQL 抽象语法树(AST)的访问接口,为访问各种 SQL 语法元素提供了统一的方法。此外,JSQLParser 还提供了一系列更具体的访问接口:

  • StatementVisitor:用于访问不同类型的 SQL 语句(如 SELECT、UPDATE、DELETE 等)
  • ExpressionVisitor:用于访问各种表达式(如条件表达式、算术表达式等)
  • SelectVisitor:专门用于访问 SELECT 语句的各个部分
  • FromItemVisitor:用于访问 FROM 子句中的各种表引用
  • SelectItemVisitor:用于访问 SELECT 列表中的各个项目
  • ItemsListVisitor:用于访问项目列表(如 IN 表达式中的值列表)

1.2 访问者实现体系

为了简化开发,JSQLParser 提供了多个适配器类,实现了上述接口的所有方法(通常为空实现),开发者可以继承这些适配器,只重写自己关心的方法。

二、TablesNamesFinder 核心原理

TablesNamesFinder 是 JSQLParser 库中提供的一个实用工具类,它的核心功能是遍历解析后的 SQL 语句或子句对象(Select,Where…)的所有节点并收集其中引用的所有表名,并提供通过重写访问者方法来扩展或定制 SQL 处理逻辑的能力。

2.1 类的继承关系

TablesNamesFinder 通过继承一系列适配器类来实现其功能,而不是直接实现 CCJSqlParserVisitor 接口。它的主要继承路径为:

// TablesNamesFinder 继承了多个访问者接口,用于处理不同类型的 SQL 元素
StatementVisitor, ExpressionVisitor, SelectVisitor, FromItemVisitor, SelectItemVisitor, ItemsListVisitor <-- TablesNamesFinder

2.2 核心功能

  1. 表名收集:自动遍历 SQL 语句的各个部分,收集所有引用的表名到内部集合中
  2. 可配置性:通过 init() 方法可以配置是否处理表的别名
  3. 便捷访问:提供 getTableList() 等方法,方便获取收集到的表名信息
  4. 可扩展性:允许子类重写特定的 visit 方法,实现自定义的 SQL 处理逻辑

三、TablesNamesFinder 与 CCJSqlParserVisitor 的关系与区别

特性TablesNamesFinderCCJSqlParserVisitor
类型具体工具类SQL抽象语法树(AST)核心接口
设计目的用于收集SQL语句中的表名,
更重要的是提供了定制化的处理能力
定义访问SQL抽象语法树的标准方法
接口实现通过实现不同节点的访问者接口,
遍历所有的SQL语法元素(比如 Table,Column,Expression)
顶层接口,定义访问各种SQL语法原始元素的方法
使用场景提取SQL中使用的表、分析表依赖关系作为实现自定义SQL处理逻辑的基础接口
执行阶段在SQL解析为语法对象上执行SQL解析阶段被调用

四、TablesNamesFinder 使用场景

  • SQL 表依赖分析

通过 TablesNamesFinder 可以快速提取 SQL 语句中引用的所有表名,用于分析 SQL 的表依赖关系,这在数据库迁移、表结构变更影响分析等场景中非常有用。

  • SQL 安全性检查

可以基于 TablesNamesFinder 构建安全检查器,识别 SQL 中是否引用了敏感表,或是否包含未授权访问的表。

  • SQL 格式化和规范化

通过扩展 TablesNamesFinder,可以实现 SQL 语句的格式化和规范化,例如为列名添加表名前缀、标准化表别名等。

  • SQL 重构和转换

基于 TablesNamesFinder 可以实现 SQL 的重构和转换,如自动添加 WHERE 条件、替换表名等。

五、基于 TablesNamesFinder 实现 SQL 格式化

下面,我们通过一个实际的代码示例来展示如何基于 TablesNamesFinder 实现 SQL 语句的格式化。

5.1 代码实现

以下是一个名为 CanonicalColumnVisitor 的内部类,它继承自 TablesNamesFinder,用于为 SQL 语句中的列添加表名前缀或别名:

/*** 规范化列访问器,继承自 TablesNamesFinder,用于为 SQL 语句中的列添加表名前缀或别名。* 该访问器会遍历 SQL 语句中的各个部分,包括 WHERE 子句、JOIN 条件、排序和分组等,* 为缺少表名前缀的列添加指定的表名或其别名,同时为表添加合适的别名。* 在遍历过程中,还会收集关联的表信息。*/
private static class CanonicalColumnVisitor extends TablesNamesFinder {private final String tablename;private final Map<String,FromItem> associatedTables = new HashMap<>();private final Function<String,String> aliaFunction = asAliasFunction(associatedTables);/*** 构造CanonicalColumnVisitor实例,关联表映射使用null值,适用于对完整SQL语句的处理* @param tablename 表名,用于为列名添加表名前缀*/CanonicalColumnVisitor(String tablename) {this(tablename, null);}/*** 构造CanonicalColumnVisitor实例* @param tablename 表名,用于为列名添加表名前缀* @param joinedTables 已连接的表映射,键为表名,值为对应的FromItem对象*/CanonicalColumnVisitor(String tablename, Map<String,FromItem> joinedTables) {this.tablename = tablename;if(null != joinedTables && !joinedTables.isEmpty()){this.associatedTables.putAll(joinedTables);}init(true);}@Overridepublic void visit(Column column) {/** 为列名增加表名前缀,优先使用表的别名 */Table table = column.getTable();if (!isNullOrEmpty(tablename)) {if (null == table) {String aliasName = aliaFunction.apply(tablename);column.setTable(new Table(null != aliasName ? aliasName : tablename));}}if (null != table) {Alias alias = table.getAlias();if (null == alias) {String aliasName = aliaFunction.apply(table.getName());if (null != aliasName && !aliasName.equals(table.getName())) {alias = new Alias(aliasName);table.setAlias(alias);}}}super.visit(column);}@Overridepublic void visit(PlainSelect plainSelect) {doVisitForCollectAssociatedTable(associatedTables, plainSelect.getFromItem(), plainSelect.getJoins());super.visit(plainSelect);// 处理JOIN条件、WHERE子句、ORDER BY和GROUP BY等部分if (plainSelect.getJoins() != null) {for (Join join : plainSelect.getJoins()) {for(Expression exp: join.getOnExpressions()) {exp.accept(this);}}}if (plainSelect.getWhere() != null) {plainSelect.getWhere().accept(this);}if (plainSelect.getOrderByElements() != null) {for (OrderByElement item : plainSelect.getOrderByElements()) {item.getExpression().accept(this);}}if (plainSelect.getGroupBy() != null) {plainSelect.getGroupBy().getGroupByExpressionList().accept(this);}}// 其他SQL语句类型的visit方法实现...@Overridepublic void visit(Update update) {doVisitForCollectAssociatedTable(associatedTables, update.getTable(), update.getJoins());super.visit(update);// 处理UPDATE语句的更新列和表达式update.getUpdateSets().forEach(us -> {us.getColumns().forEach(c -> c.accept(this));us.getExpressions().forEach(e -> e.accept(this));});}// Delete、Upsert等其他语句类型的visit方法实现...
}

5.2 关键辅助方法

该实现中使用了几个关键的辅助方法:

5.2.1 asAliasFunction 方法
/*** 创建一个用于获取表别名的函数* 1. 根据输入的表名,从已JOIN表映射中查找对应的FromItem对象* 2. 若找到FromItem且有别名,则返回别名* 3. 若找到FromItem但无别名,则返回原表名* 4. 若未找到FromItem,则返回null*/
private static Function<String, String> asAliasFunction(Map<String, FromItem> joinedTables) {class AliasFunction implements Function<String, String> {private final Map<String, FromItem> joinedTables;AliasFunction(Map<String, FromItem> joinedTables) {this.joinedTables = null == joinedTables ? Collections.emptyMap() : joinedTables;}@Overridepublic String apply(String name) {FromItem fromItem = null == name ? null : joinedTables.get(name);if (null == fromItem) {return null;}Alias alias = fromItem.getAlias();return (null == alias || alias.getName() == null) ? name : alias.getName();}// hashCode、equals和toString方法实现...}return new AliasFunction(joinedTables);
}
5.2.2 doVisitForCollectAssociatedTable 方法
/*** 遍历FromItem和JOIN子句,将其中的表信息添加到已连接表映射中*/
private static void doVisitForCollectAssociatedTable(Map<String, FromItem> joinedTables, FromItem fromItem, List<Join> joins) {if(null != fromItem) {joinedTables.put(tablenameOrAliasOf(fromItem), fromItem);}if(null != joins){joins.stream().map(j -> j.getRightItem()).forEach(i->joinedTables.put(tablenameOrAliasOf(i), i));}
}
5.2.3 tablenameOrAliasOf 方法
/*** 获取 FromItem 对象对应的表名或别名*/
private static String tablenameOrAliasOf(FromItem fromItem) {if(null == fromItem) {return null;}if(fromItem instanceof Table) {return ((Table)fromItem).getName();}Alias alias = fromItem.getAlias();return (null == alias) ? null : alias.getName();
}

5.3 使用示例

以下是如何使用 CanonicalColumnVisitor 来规范化 SQL 语句的示例:

/*** 规范化SQL语句对应的Statement对象* 解析传入的SQL语句,获取对应的Statement对象,并使用CanonicalColumnVisitor对其进行访问,* 为没有指定表名的字段名自动加上 tablename 指定的表名前缀*/
private static Statement normalizeStatement(String tablename, String sql) throws JSQLParserException {Statement statement = parseStatement(sql);return normalizeStatement(tablename, statement);
}/*** 规范化SQL语句对应的Statement对象* 使用CanonicalColumnVisitor对传入的Statement对象进行访问,* 为没有指定表名的字段名自动加上 tablename 指定的表名前缀*/
private static Statement normalizeStatement(String tablename, Statement statement) throws JSQLParserException {statement.accept(new CanonicalColumnVisitor(tablename));return statement;
}/*** 解析 SQL 语句字符串并返回对应的 Statement 对象*/
private static Statement parseStatement(String sql) throws JSQLParserException {// 解析SQL语句的实现return ParserSupport.parse0(sql, null, null).statement;
}/*** 规范化SQL字符串*/
private static String normalizeSql(String tablename, String sql) {if(isNullOrEmpty(sql)){return sql;}try {return normalizeStatement(tablename, sql).toString();} catch (JSQLParserException e) {// 解析SQL语句失败,不做任何处理返回原值return sql;}
}

六、实际应用场景分析

6.1 SQL 格式化与规范化

在多表联合查询中,为每个列添加表名前缀可以避免列名歧义,提高 SQL 语句的可读性和可维护性。通过 CanonicalColumnVisitor,我们可以自动为 SQL 语句中的列添加表名前缀。

输入输出示例

输入:

SELECT id, name FROM user WHERE age > 18 ORDER BY create_time;

输出(使用 user 作为表名前缀):

SELECT user.id, user.name FROM user WHERE user.age > 18 ORDER BY user.create_time;

6.2 SQL 安全增强

基于 TablesNamesFinder,我们可以实现 SQL 安全增强功能,如自动为 SQL 添加访问控制条件:

/*** 为 SQL 语句添加访问限制条件*/
private static Statement addLimitForStatement(Statement statement, String joinClause, String limitExpression) {statement.accept(new TablesNamesFinder(){{init(true);}@Overridepublic void visit(PlainSelect plainSelect) {super.visit(plainSelect);doVisitForAddLimit(limitExpression, joinClause, plainSelect::getWhere, plainSelect::getJoins, plainSelect::setWhere, plainSelect::setJoins);}@Overridepublic void visit(Delete delete) {super.visit(delete);doVisitForAddLimit(limitExpression, joinClause, delete::getWhere, delete::getJoins, delete::setWhere, delete::setJoins);}@Overridepublic void visit(Update update) {super.visit(update);doVisitForAddLimit(limitExpression, joinClause, update::getWhere, update::getJoins, update::setWhere, update::setJoins);}});return statement;
}

七、总结

TablesNamesFinder 是 JSQLParser 提供的一个强大工具类,通过继承和扩展它,我们可以实现各种复杂的 SQL 处理功能。本文通过实际代码示例,展示了如何基于 TablesNamesFinder 实现 SQL 的格式化和规范化处理。

相对于通用的 CCJSqlParserVisitorTablesNamesFinder 使用更加简单便捷,特别适合于需要对 SQL 表引用进行分析和处理的场景。

在实际应用中,我们可以根据具体需求,进一步扩展 TablesNamesFinder,实现更复杂的 SQL 处理逻辑,如 SQL 重构、SQL 优化建议、SQL 安全检查等功能。

jsqlparser系列文章

《jsqlparser(一):基于抽象语法树(AST)遍历SQL语句的语法元素》
《jsqlparser(二):实现基于SQL语法分析的SQL注入攻击检查》
《jsqlparser(三):基于语法分析实现SQL中的CAST函数替换》
《jsqlparser(四):实现MySQL 函数DATE_FORMAT到Phoenix函数TO_CHAR的替换》
《jsqlparser(五):修改语法定义(JSqlParserCC.jjt)实现UPSERT支持Phoenix语法ON DUPLICATE KEY IGNORE》
《jsqlparser(六):TablesNamesFinder 深度解析与 SQL 格式化实现》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/94994.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/94994.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python训练营打卡Day49-神经网络调参指南

知识点回顾&#xff1a;随机种子内参的初始化神经网络调参指南 参数的分类调参的顺序各部分参数的调整心得 作业&#xff1a;对于day41的简单cnn&#xff0c;看看是否可以借助调参指南进一步提高精度。 随机种子 import torch import torch.nn as nn# 定义简单的线性模型&…

Elasticsearch 常用任务管理命令及实战应用

常用任务管理命令 列出所有任务 curl -X GET "http://<es_host>:<es_port>/_tasks?detailedtrue&pretty" -H Content-Type: application/json获取特定类型的任务 curl -X GET "http://<es_host>:<es_port>/_tasks?actions<act…

Java试题-选择题(26)

Java试题-选择题(26) 题目 下列有关Thread的描述,哪个是正确的 ? A:启动一个线程的方法是:thread. run() B:结束一个线程的通常做法是:thread. stop() C:将一个线程标记成daemon线程,意味着当主线程结束,并且没有其它正在运行的非daemon线程时,该daemon线程也会自…

缓存的原理、引入及设计

开篇寄语&#xff1a;缓存&#xff0c;你真的用对了吗&#xff1f; 我们为什么要学习缓存呢&#xff1f;有必要学习缓存吗&#xff1f; 缓存的使用&#xff0c;是提升系统性能、改善用户体验的唯一解决之道。 其实&#xff0c;作为互联网公司&#xff0c;只要有直接面对用户的业…

单片机如何控制模数转换芯片

一、介绍单片机控制模数转换&#xff08;ADC&#xff09;芯片的核心是通过通信接口发送控制指令&#xff0c;并读取转换后的数字信号&#xff0c;本质是“指令交互数据传输”的协同过程&#xff0c;具体实现需分4步完成&#xff0c;关键在于接口匹配和时序同步。二、核心1. 先明…

【Proteus仿真】开关控制系列仿真——开关控制LED/拨码开关二进制计数/开关和继电器控制灯灭

目录 0案例视频效果展示 0.1例子1&#xff1a;开关控制LED灯亮灭 0.2例子2&#xff1a;数码管显示拨码开关二进制计数(000~255) 0.3例子3&#xff1a;开关和继电器控制灯亮灭 1基础知识补充 1.1 74LS245双总线收发器 1.1.1 引脚及功能 1.1.2应用场景 1.1.3真值表 1.2…

Q1 Top IF 18.7 | 基于泛基因组揭示植物NLR进化

文章DOI: 10.1016/j.chom.2025.07.011 标题&#xff1a;Pangenomic context reveals the extent of intraspecific plant NLR evolution 期刊&#xff1a;Cell Hose & Microbe (https://i-blog.csdnimg.cn/direct/0e31f86b94d348b0a1adb084ec4e49b7.png)(https://i-blog.cs…

技术干货|Prometheus PromQL查询语言之聚合操作内置函数

聚合操作 Prometheus还提供了下列内置的聚合操作符,这些操作符作用域瞬时向量。可以将瞬时表达式返回的样本数据进行聚合,形成一个新的时间序列。 sum (求和) min (最小值) max (最大值) avg (平均值) stddev (标准差) stdvar (标准差异) count (计数) count_values …

Redis 哨兵(Sentinel)全面解析

在2025年的数字化浪潮中&#xff0c;想象这样一个场景&#xff1a;凌晨3点&#xff0c;电商平台流量突然暴增&#xff0c;主Redis服务器因硬件故障突然宕机。几年前&#xff0c;这意味着紧急电话、慌乱的运维人员和不可避免的业务中断。而今天&#xff0c;用户甚至没有察觉任何…

【数学史冷知识】关于行列式的发展史

学习的途中会遇到一些有意思的东西&#xff0c;我想着做一个专栏《艾萨克纪行简报》&#xff0c;专门写这些知识发展历史。可以让您从繁忙的学习生活中放松&#xff0c;添些耀彩。行列式和微积分一样&#xff0c;都是两个人独立发现的。而且还都有莱布尼茨。1683 年&#xff0c…

【python】python进阶——生成器

目录 一、生成器介绍 1.1 生成器与迭代器的关系 1.2 生成器与return比较 二、创建生成器 方法1: 生成器函数 方法2: 生成器表达式 三、生成器的实际应用场景 3.1 处理大型文件 3.2 生成无限序列 3.3 数据管道处理 四、生成器的高级用法 4.1 使用send()方法传递值 …

【Pytorch】生成对抗网络实战

GAN框架基于两个模型的竞争&#xff0c;Generator生成器和Discriminator鉴别器。生成器生成假图像&#xff0c;鉴别器则尝试从假图像中识别真实的图像。作为这种竞争的结果&#xff0c;生成器将生成更好看的假图像&#xff0c;而鉴别器将更好地识别它们。 目录 创建数据集 定…

Java基础第7天总结(代码块、内部类、函数式编程)

代码块静态代码块&#xff1a;有static修饰&#xff0c;属于类&#xff0c;与类一起优先加载&#xff0c;自动执行一次实例代码块&#xff1a;无static修饰&#xff0c;属于对象&#xff0c;每次创建对象时&#xff0c;都会优先执行一次。package com.itheima.code;import java…

文献综述写作指南:从海量文献到逻辑闭环的实战模板

文献综述往往是学术写作的“第一关难题”&#xff1a;面对成百上千篇文献&#xff0c;如何避免“简单罗列”的陷阱&#xff0c;梳理出有逻辑、有洞见的论述体系&#xff1f;本文结合学术写作实践&#xff0c;总结出一套模块化的文献综述“实战模板”&#xff0c;通过结构化方法…

CuTe C++ 简介01,从示例开始

这里先仅仅关注 C 层的介绍&#xff0c;python DSL 以后再说。在 ubuntu 22.04 X64 中&#xff0c;RTX 50801. 环境搭建1.1 安装 cuda1.2 下载源码git clone https://github.com/NVIDIA/cutlass.git1.3 编译mkdir build/ cmake .. -DCUTLASS_NVCC_ARCHS"120" -DCMAK…

Python实现异步多线程Web服务器:从原理到实践

目录Python实现异步多线程Web服务器&#xff1a;从原理到实践引言第一章&#xff1a;Web服务器基础1.1 Web服务器的工作原理1.2 HTTP协议简介1.3 同步 vs 异步 vs 多线程第二章&#xff1a;Python异步编程基础2.1 异步I/O概念2.2 协程与async/await2.3 事件循环第三章&#xff…

Deep Think with Confidence:llm如何进行高效率COT推理优化

1. 引言:大模型的推理解码优化 大型语言模型(LLM)在处理数学、编码等复杂推理任务时,一种强大但“耗能巨大”的技术是self-consistency,也称并行思考(parallel thinking)。其核心思想是让模型对同一个问题生成多条不同的“思考路径”(reasoning traces),然后通过多数…

vscode克隆远程代码步骤

一、直接使用VsCode1.复制git的https链接代码2.在vscode中点击 代码管理-克隆仓库3.粘贴&#xff08;在git里面复制的https链接&#xff09;4.选择需要存储的文件位置5.确认6.代码克隆成功二、使用命令行克隆1.确定文件放置位置&#xff0c;右键2.复制git的https链接代码3.粘贴…

spi总线

一、介绍SPI总线&#xff08;Serial Peripheral Interface&#xff0c;串行外设接口&#xff09;是一种高速全双工同步串行通信总线&#xff0c;核心通过“主从架构同步时钟”实现设备间数据传输&#xff0c;因结构简单、速率高&#xff0c;广泛用于MCU与传感器、存储芯片、显示…

COLA:大型语言模型高效微调的革命性框架

本文由「大千AI助手」原创发布&#xff0c;专注用真话讲AI&#xff0c;回归技术本质。拒绝神话或妖魔化。搜索「大千AI助手」关注我&#xff0c;一起撕掉过度包装&#xff0c;学习真实的AI技术&#xff01; 1 COLA技术概述 COLA&#xff08;Chain of LoRA&#xff09;是一种创…