druid 数据源【七、子查询】
Druid 各种子查询 详解
一、先记牢:Druid 中子查询的真实类型(必背,无虚构类)
子查询的核心区别在于「出现位置」和「作用」,对应不同的 Druid 类,以下是完整对应关系,直接记熟即可避坑:
| 子查询位置 | SQL 写法示例 | 对应 Druid 类 | 所属父类(类型) | 核心作用 |
|---|---|---|---|---|
| WHERE 子句(条件中) | id IN (SELECT user_id FROM order) | SQLInSubQueryExpr | SQLExpr(表达式) | 判断字段是否在子查询结果中 |
| EXISTS (SELECT 1 FROM order WHERE user_id = id) | SQLExistsExpr | SQLExpr(表达式) | 判断子查询是否有结果 | |
| age > ANY (SELECT age FROM vip) | SQLAnyExpr | SQLExpr(表达式) | 判断字段是否大于子查询中任意一个结果 | |
| age > ALL (SELECT age FROM vip) | SQLAllExpr | SQLExpr(表达式) | 判断字段是否大于子查询中所有结果 | |
| age = (SELECT MAX(age) FROM user) | SQLBinaryOpExpr(嵌套 SQLQueryExpr) | SQLExpr(表达式) | 用比较运算符(=、>、< 等)连接子查询 | |
| FROM 子句(表来源) | FROM (SELECT id, name FROM user) AS t | SQLSubqueryTableSource | SQLTableSource(表来源) | 子查询作为派生表,供主查询关联 |
| SELECT 列表(字段中) | SELECT id, (SELECT name FROM user WHERE id=1) FROM order | SQLQueryExpr | SQLExpr(表达式) | 标量子查询,返回单个值作为查询列 |
二、环境准备(必配,否则解析失败)
2.1 Maven 依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.20</version> <!-- 稳定版本,推荐使用 -->
</dependency>
2.2 通用导入包
所有示例均需导入以下包,统一复制即可:
import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.SQLSelect;
import com.alibaba.druid.sql.ast.expr.*;
import com.alibaba.druid.sql.ast.statement.*;
import com.alibaba.druid.util.JdbcConstants;
import java.util.List;
三、分场景实战:所有子查询解析 + 使用示例
按「使用频率」排序,每个场景均提供「SQL 示例 + 解析代码 + 结果说明」,可直接复制运行,重点补充「比较运算符子查询」细节。
场景 1:WHERE 后的子查询(最常用,5 种细分场景)
WHERE 子查询的核心是「作为条件判断」,所有相关类均继承自 SQLExpr,解析时需先获取 WHERE 表达式,再判断具体类型。
1.1 IN 子查询:id IN (SELECT ...)(对应 SQLInSubQueryExpr)
最常用的子查询场景,判断字段值是否在子查询返回的结果集中。
示例:解析 IN 子查询
public class SubQueryDemo1 {
public static void main(String[] args) {
// SQL:WHERE 后 IN 子查询
String sql = "SELECT id, name FROM user WHERE id IN (SELECT user_id FROM `order` WHERE status = 1)";
// 1. 解析 SQL 语句
SQLSelectStatement stmt = (SQLSelectStatement) SQLUtils.parseSingleStatement(sql, JdbcConstants.MYSQL);
// 2. 获取主查询块
SQLSelectQueryBlock mainBlock = stmt.getSelect().getQueryBlock();
// 3. 获取 WHERE 条件表达式
SQLExpr whereExpr = mainBlock.getWhere();
// 4. 判断是否为 IN 子查询
if (whereExpr instanceof SQLInSubQueryExpr) {
SQLInSubQueryExpr inSubQuery = (SQLInSubQueryExpr) whereExpr;
// 提取左侧字段(id)
SQLExpr leftCol = inSubQuery.getExpr();
// 提取子查询对象
SQLSelect subSelect = inSubQuery.getSubQuery();
// 提取子查询的查询块(方便进一步解析子查询的条件、表等)
SQLSelectQueryBlock subBlock = subSelect.getQueryBlock();
// 打印结果
System.out.println("左侧判断字段:" + SQLUtils.toSQLString(leftCol));
System.out.println("IN 子查询完整 SQL:" + SQLUtils.toSQLString(subSelect));
System.out.println("子查询关联的表:" + SQLUtils.toSQLString(subBlock.getFrom()));
System.out.println("子查询条件:" + SQLUtils.toSQLString(subBlock.getWhere()));
}
}
}
运行结果
左侧判断字段:id
IN 子查询完整 SQL:SELECT user_id FROM `order` WHERE status = 1
子查询关联的表:`order`
子查询条件:status = 1
1.2 EXISTS 子查询:EXISTS (SELECT ...)(对应 SQLExistsExpr)
判断子查询是否返回结果(无需关注具体结果,只看是否有数据),支持 NOT EXISTS。
示例:解析 EXISTS 子查询 + 改写(添加子查询条件)
public class SubQueryDemo2 {
public static void main(String[] args) {
// SQL:WHERE 后 EXISTS 子查询(带关联条件)
String sql = "SELECT id, name FROM user u WHERE EXISTS (SELECT 1 FROM `order` o WHERE o.user_id = u.id)";
SQLSelectStatement stmt = (SQLSelectStatement) SQLUtils.parseSingleStatement(sql, JdbcConstants.MYSQL);
SQLExpr whereExpr = stmt.getSelect().getQueryBlock().getWhere();
// 判断是否为 EXISTS 子查询
if (whereExpr instanceof SQLExistsExpr) {
SQLExistsExpr existsExpr = (SQLExistsExpr) whereExpr;
// 提取子查询
SQLSelect subSelect = existsExpr.getSubQuery();
SQLSelectQueryBlock subBlock = subSelect.getQueryBlock();
System.out.println("原 EXISTS 子查询:" + SQLUtils.toSQLString(subSelect));
System.out.println("是否为 NOT EXISTS:" + existsExpr.isNot());
// 改写:给子查询添加条件(order.create_time > '2024-01-01')
SQLExpr newCond = SQLUtils.toSQLExpr("o.create_time > '2024-01-01'", JdbcConstants.MYSQL);
subBlock.addWhere(newCond);
// 输出改写后的完整 SQL
System.out.println("改写后完整 SQL:" + SQLUtils.toSQLString(stmt));
}
}
}
运行结果
原 EXISTS 子查询:SELECT 1 FROM `order` o WHERE o.user_id = u.id
是否为 NOT EXISTS:false
改写后完整 SQL:SELECT id, name FROM user u WHERE EXISTS (SELECT 1 FROM `order` o WHERE o.user_id = u.id AND o.create_time > '2024-01-01')
1.3 ANY / ALL 子查询(对应 SQLAnyExpr / SQLAllExpr)
用于「字段与子查询结果集的批量比较」:
-
ANY:字段只需满足子查询中「任意一个」结果即可
-
ALL:字段需满足子查询中「所有」结果才可
示例:解析 ANY / ALL 子查询
public class SubQueryDemo3 {
public static void main(String[] args) {
// 1. ANY 子查询示例
String sql1 = "SELECT id, name FROM user WHERE age > ANY (SELECT age FROM vip WHERE level = 1)";
// 2. ALL 子查询示例
String sql2 = "SELECT id, name FROM user WHERE age > ALL (SELECT age FROM vip WHERE level = 1)";
// 解析 ANY 子查询
parseAnyAllSubQuery(sql1);
System.out.println("--------------------------");
// 解析 ALL 子查询
parseAnyAllSubQuery(sql2);
}
private static void parseAnyAllSubQuery(String sql) {
SQLSelectStatement stmt = (SQLSelectStatement) SQLUtils.parseSingleStatement(sql, JdbcConstants.MYSQL);
SQLExpr whereExpr = stmt.getSelect().getQueryBlock().getWhere();
if (whereExpr instanceof SQLAnyExpr) {
SQLAnyExpr anyExpr = (SQLAnyExpr) whereExpr;
System.out.println("子查询类型:ANY 子查询");
System.out.println("子查询 SQL:" + SQLUtils.toSQLString(anyExpr.getSubQuery()));
} else if (whereExpr instanceof SQLAllExpr) {
SQLAllExpr allExpr = (SQLAllExpr) whereExpr;
System.out.println("子查询类型:ALL 子查询");
System.out.println("子查询 SQL:" + SQLUtils.toSQLString(allExpr.getSubQuery()));
}
}
}
运行结果
子查询类型:ANY 子查询
子查询 SQL:SELECT age FROM vip WHERE level = 1
--------------------------
子查询类型:ALL 子查询
子查询 SQL:SELECT age FROM vip WHERE level = 1
1.4 比较运算符子查询(重点补充,对应 SQLBinaryOpExpr + SQLQueryExpr)
最容易被忽略的子查询场景:用「=、>、<、>=、<=、!=」等比较运算符,直接连接字段和子查询(子查询需返回单个值,即标量子查询)。
核心逻辑:比较运算符子查询本质是「二元运算表达式(SQLBinaryOpExpr)」,右侧是「标量子查询(SQLQueryExpr)」,解析时需先获取二元运算的右侧表达式,再判断是否为标量子查询。
常见 SQL 示例
-
age = (SELECT MAX(age) FROM user) → 等于子查询结果(最大值)
-
create_time > (SELECT MIN(create_time) FROM
order) → 大于子查询结果(最小值) -
salary <= (SELECT AVG(salary) FROM dept WHERE dept_id = 10) → 小于等于子查询结果(平均值)
示例:解析 + 改写比较运算符子查询
public class SubQueryDemo4 {
public static void main(String[] args) {
// SQL:比较运算符子查询(= 连接,子查询返回单个值)
String sql = "SELECT id, name FROM user WHERE age = (SELECT MAX(age) FROM user WHERE dept_id = 3)";
SQLSelectStatement stmt = (SQLSelectStatement) SQLUtils.parseSingleStatement(sql, JdbcConstants.MYSQL);
SQLSelectQueryBlock mainBlock = stmt.getSelect().getQueryBlock();
SQLExpr whereExpr = mainBlock.getWhere();
// 关键:比较运算符子查询是 SQLBinaryOpExpr(二元运算)
if (whereExpr instanceof SQLBinaryOpExpr) {
SQLBinaryOpExpr binaryOp = (SQLBinaryOpExpr) whereExpr;
// 左侧:字段(age)
SQLExpr leftExpr = binaryOp.getLeft();
// 右侧:子查询(SELECT MAX(age) ...),类型是 SQLQueryExpr
SQLExpr rightExpr = binaryOp.getRight();
// 判断右侧是否为标量子查询
if (rightExpr instanceof SQLQueryExpr) {
SQLQueryExpr queryExpr = (SQLQueryExpr) rightExpr;
SQLSelect subSelect = queryExpr.getSubQuery();
SQLSelectQueryBlock subBlock = subSelect.getQueryBlock();
// 打印解析结果
System.out.println("比较运算符:" + binaryOp.getOperator());
System.out.println("左侧字段:" + SQLUtils.toSQLString(leftExpr));
System.out.println("右侧标量子查询:" + SQLUtils.toSQLString(subSelect));
System.out.println("子查询条件:" + SQLUtils.toSQLString(subBlock.getWhere()));
// 改写:修改子查询条件(dept_id 从 3 改为 4)
subBlock.setWhere(SQLUtils.toSQLExpr("dept_id = 4", JdbcConstants.MYSQL));
System.out.println("改写后完整 SQL:" + SQLUtils.toSQLString(stmt));
}
}
}
}
运行结果
比较运算符:Equality(=)
左侧字段:age
右侧标量子查询:SELECT MAX(age) FROM user WHERE dept_id = 3
子查询条件:dept_id = 3
改写后完整 SQL:SELECT id, name FROM user WHERE age = (SELECT MAX(age) FROM user WHERE dept_id = 4)
场景 2:FROM 后的子查询(派生表,对应 SQLSubqueryTableSource)
核心特点:子查询作为「表来源」,必须给子查询起别名(否则 SQL 语法错误),对应 Druid 类是 SQLSubqueryTableSource,不属于 SQLExpr,属于 SQLTableSource(和普通表、联表的父类一致)。
示例:解析 FROM 子查询 + 提取子查询信息 + 改写子查询
public class SubQueryDemo5 {
public static void main(String[] args) {
// SQL:FROM 后子查询(派生表),带别名 t
String sql = "SELECT t.id, t.name, t.age FROM (SELECT id, name, age FROM user WHERE age > 18) AS t WHERE t.age < 30";
SQLSelectStatement stmt = (SQLSelectStatement) SQLUtils.parseSingleStatement(sql, JdbcConstants.MYSQL);
SQLSelectQueryBlock mainBlock = stmt.getSelect().getQueryBlock();
// 获取 FROM 后的表来源(子查询派生表)
SQLTableSource tableSource = mainBlock.getFrom();
// 判断是否为 FROM 子查询
if (tableSource instanceof SQLSubqueryTableSource) {
SQLSubqueryTableSource subTable = (SQLSubqueryTableSource) tableSource;
// 提取关键信息
String alias = subTable.getAlias(); // 子查询别名 t
SQLSelect subSelect = subTable.getSelect(); // 子查询对象
SQLSelectQueryBlock subBlock = subSelect.getQueryBlock(); // 子查询的查询块
System.out.println("FROM 子查询别名:" + alias);
System.out.println("FROM 子查询完整 SQL:" + SQLUtils.toSQLString(subSelect));
System.out.println("子查询查询列:" + SQLUtils.toSQLString(subBlock.getSelectList()));
System.out.println("子查询条件:" + SQLUtils.toSQLString(subBlock.getWhere()));
// 改写:给子查询添加条件(name LIKE '%张%')
SQLExpr newCond = SQLUtils.toSQLExpr("name LIKE '%张%'", JdbcConstants.MYSQL);
subBlock.addWhere(newCond);
System.out.println("改写后完整 SQL:" + SQLUtils.toSQLString(stmt));
}
}
}
运行结果
FROM 子查询别名:t
FROM 子查询完整 SQL:SELECT id, name, age FROM user WHERE age > 18
子查询查询列:id, name, age
子查询条件:age > 18
改写后完整 SQL:SELECT t.id, t.name, t.age FROM (SELECT id, name, age FROM user WHERE age > 18 AND name LIKE '%张%') AS t WHERE t.age < 30
场景 3:SELECT 列表中的标量子查询(对应 SQLQueryExpr)
核心特点:子查询作为「查询列」,返回单个值(标量),对应 Druid 类是 SQLQueryExpr,属于 SQLExpr,通常嵌套在 SQLSelectItem 中。
示例:解析 SELECT 标量子查询 + 遍历所有查询列
public class SubQueryDemo6 {
public static void main(String[] args) {
// SQL:SELECT 列表中包含 2 个标量子查询
String sql = "SELECT " +
"id, " +
"(SELECT name FROM user WHERE user.id = order.user_id) AS user_name, " +
"(SELECT dept_name FROM dept WHERE dept.id = user.dept_id) AS dept_name " +
"FROM `order`";
SQLSelectStatement stmt = (SQLSelectStatement) SQLUtils.parseSingleStatement(sql, JdbcConstants.MYSQL);
SQLSelectQueryBlock mainBlock = stmt.getSelect().getQueryBlock();
// 遍历所有查询列,识别标量子查询
List<SQLSelectItem> selectItems = mainBlock.getSelectList();
for (int i = 0; i < selectItems.size(); i++) {
SQLSelectItem item = selectItems.get(i);
SQLExpr expr = item.getExpr();
String columnAlias = item.getAlias(); // 查询列别名
System.out.println("第 " + (i+1) + " 列:");
System.out.println(" 列别名:" + (columnAlias == null ? "无" : columnAlias));
// 判断是否为标量子查询
if (expr instanceof SQLQueryExpr) {
SQLQueryExpr queryExpr = (SQLQueryExpr) expr;
SQLSelect subSelect = queryExpr.getSubQuery();
System.out.println(" 类型:标量子查询");
System.out.println(" 子查询 SQL:" + SQLUtils.toSQLString(subSelect));
} else {
System.out.println(" 类型:普通字段");
System.out.println(" 字段名:" + SQLUtils.toSQLString(expr));
}
System.out.println("--------------------------");
}
}
}
运行结果
第 1 列:
列别名:无
类型:普通字段
字段名:id
--------------------------
第 2 列:
列别名:user_name
类型:标量子查询
子查询 SQL:SELECT name FROM user WHERE user.id = order.user_id
--------------------------
第 3 列:
列别名:dept_name
类型:标量子查询
子查询 SQL:SELECT dept_name FROM dept WHERE dept.id = user.dept_id
--------------------------
四、高级实战:子查询通用解析工具类(企业级,直接复制使用)
封装通用方法,支持「自动识别所有子查询类型、提取子查询信息、递归解析多层子查询」,适配所有场景,可直接集成到项目中。
import com.alibaba.druid.sql.ast.SQLObject;
import com.alibaba.druid.sql.visitor.SQLASTVisitorAdapter;
/**
* Druid 子查询通用解析工具类
* 支持:所有子查询类型识别、信息提取、多层子查询递归解析
*/
public class SubQueryUtils {
/**
* 解析 SQL 中的所有子查询
*/
public static void parseAllSubQueries(String sql) {
SQLSelectStatement stmt = (SQLSelectStatement) SQLUtils.parseSingleStatement(sql, JdbcConstants.MYSQL);
// 用 Visitor 模式遍历整个 AST,识别所有子查询
stmt.accept(new SQLASTVisitorAdapter() {
@Override
public boolean visit(SQLInSubQueryExpr x) {
System.out.println("[WHERE IN 子查询]");
System.out.println(" 左侧字段:" + SQLUtils.toSQLString(x.getExpr()));
System.out.println(" 子查询 SQL:" + SQLUtils.toSQLString(x.getSubQuery()));
System.out.println();
return true; // 继续遍历子节点(子查询内部)
}
@Override
public boolean visit(SQLExistsExpr x) {
System.out.println("[WHERE EXISTS 子查询]");
System.out.println(" 是否 NOT EXISTS:" + x.isNot());
System.out.println(" 子查询 SQL:" + SQLUtils.toSQLString(x.getSubQuery()));
System.out.println();
return true;
}
@Override
public boolean visit(SQLAnyExpr x) {
System.out.println("[WHERE ANY 子查询]");
System.out.println(" 子查询 SQL:" + SQLUtils.toSQLString(x.getSubQuery()));
System.out.println();
return true;
}
@Override
public boolean visit(SQLAllExpr x) {
System.out.println("[WHERE ALL 子查询]");
System.out.println(" 子查询 SQL:" + SQLUtils.toSQLString(x.getSubQuery()));
System.out.println();
return true;
}
@Override
public boolean visit(SQLSubqueryTableSource x) {
System.out.println("[FROM 派生表子查询]");
System.out.println(" 子查询别名:" + x.getAlias());
System.out.println(" 子查询 SQL:" + SQLUtils.toSQLString(x.getSelect()));
System.out.println();
return true;
}
@Override
public boolean visit(SQLQueryExpr x) {
System.out.println("[SELECT 标量子查询]");
System.out.println(" 子查询 SQL:" + SQLUtils.toSQLString(x.getSubQuery()));
System.out.println();
return true;
}
});
}
/**
* 判断子查询类型(返回中文描述)
*/
public static String getSubQueryType(SQLObject obj) {
if (obj instanceof SQLInSubQueryExpr) {
return "WHERE IN 子查询";
} else if (obj instanceof SQLExistsExpr) {
return "WHERE EXISTS 子查询";
} else if (obj instanceof SQLAnyExpr) {
return "WHERE ANY 子查询";
} else if (obj instanceof SQLAllExpr) {
return "WHERE ALL 子查询";
} else if (obj instanceof SQLSubqueryTableSource) {
return "FROM 派生表子查询";
} else if (obj instanceof SQLQueryExpr) {
return "SELECT 标量子查询";
} else {
return "非子查询对象";
}
}
// 测试
public static void main(String[] args) {
String sql = "SELECT " +
"id, " +
"(SELECT name FROM user WHERE user.id = order.user_id) AS user_name " +
"FROM (SELECT id, user_id FROM `order` WHERE status = 1) AS t " +
"WHERE id IN (SELECT order_id FROM pay WHERE amount > 100) " +
"AND EXISTS (SELECT 1 FROM log WHERE log.order_id = t.id) " +
"AND amount > ANY (SELECT amount FROM pay WHERE status = 0)";
parseAllSubQueries(sql);
}
}
五、避坑指南(必看,避免解析失败)
-
必须指定数据库方言:解析时一定要传入 JdbcConstants.MYSQL / ORACLE 等,否则子查询解析会出现异常(比如无法识别 MySQL 特有的语法)。
-
区分 SQLQueryExpr 和 SQLSubqueryTableSource:前者是「标量子查询(表达式)」,后者是「派生表(表来源)」,不要混淆两者的父类。
-
比较运算符子查询的解析逻辑:先判断是否为 SQLBinaryOpExpr,再判断右侧是否为 SQLQueryExpr,不能直接判断为 SQLQueryExpr(因为它是嵌套在二元运算中的)。
-
子查询别名:FROM 后的子查询必须有别名,否则解析时不会报错,但执行 SQL 时会语法错误,工具类中可添加别名校验。
-
多层子查询:用 Visitor 模式可递归解析多层子查询(比如子查询中又包含子查询),无需手动递归。
六、总结(最精简,方便记忆)
6.1 核心子查询类(真实存在,无虚构)
-
WHERE 子查询:SQLInSubQueryExpr(IN)、SQLExistsExpr(EXISTS)、SQLAnyExpr(ANY)、SQLAllExpr(ALL)、SQLBinaryOpExpr+SQLQueryExpr(比较运算符)
-
FROM 子查询:SQLSubqueryTableSource(派生表)
-
SELECT 子查询:SQLQueryExpr(标量子查询)
6.2 核心逻辑
子查询的解析本质是「识别子查询的位置 + 对应 Druid 类」,所有子查询最终都会被解析成对应的 Java 对象,通过这些对象可以提取子查询的 SQL、条件、表等信息,也可以进行改写(比如添加多租户条件、过滤条件)。
6.3 常用场景
企业开发中,子查询解析常用于「SQL 审计(提取子查询关联表)、SQL 改写(给子查询加条件)、数据权限控制(拦截非法子查询)」等场景,本文提供的工具类可直接适配这些场景。
本文来自博客园,作者:蓝迷梦,转载请注明原文链接:https://www.cnblogs.com/hewei-blogs/articles/19995415

浙公网安备 33010602011771号