注:内容均以java实现
这件事情,其实我们可以通过jsqlparser这款良心工具来完成。但是拿人家的东西来用就没机会锻炼自己了,正好最近工作上碰到了这个问题,写了一个工具类,也顺便记录下吧。
为了不必要的麻烦,这里做一个前提假设:输入的sql没有语法错误。
一、提取表名
虽然sql没有语法错误,但无法保证sql中带有注释符号,有的sql还带着很大的空行以及很长的空格。【我们公司的客服和老板都热爱能写sql,他们就是慢sql和不规范sql的缔造者,甚至一度引起公司内部服务器瘫痪,嘘!】
整个实现还是相当容易的
言归正传,先把sql按一个规则垂直分割,请围观java api 中的这个符号\v
// 1. 先把所有的sql切成一行一行的样子,每一行都去除前后空格,以及注释符号,然后放入list
private List<String> getSqlLines(String originSql) {
String[] lines = originSql.split("\\v");
List<String> newLines = new ArrayList<>();
for (String line : lines) {
String trimLine = line.trim();
if ("".equals(trimLine) || trimLine.startsWith("--") || trimLine.startsWith("#")) {
continue;
}
newLines.add(trimLine);
}
return newLines;
}
// 2. 将分行的sql重新连成一行
private String getInlineSql(String originSql) {
List<String> newLines = getSqlLines(originSql);
StringBuilder sql = new StringBuilder();
newLines.forEach(str -> sql.append(str).append(" "));
return sql.toString();
}
// 3. 从sql中获取表名【将第2步返回的sql作为这一步的输入】
public Set<String> getTablesFromSql(String originSql) {
Set<String> tableSet = new TreeSet<>();
String sql = getInlineSql(originSql);
// 首先,表名肯定是跟在这些关键词后面的
String[] sqlFrag = sql.split("from|join|FROM|JOIN");
for (int i = 1; i < sqlFrag.length; i++) {
String trimFrag = sqlFrag[i].trim();
// 如果有子查询的话,那么from后面就是跟着 '(' 但这不是我们想要的,因此pass
if (!trimFrag.startsWith("(")) {
String tableName = trimFrag.split(" ")[0];
if (tableName.contains("`")) {
tableName = tableName.replaceAll("`", "");
}
// 这里是将表名前面的限定名去掉了,可以根据自己的需求来
if (tableName.contains(".")) {
tableName = tableName.substring(tableName.indexOf('.') + 1);
}
tableSet.add(tableName);
}
}
return tableSet;
}
二、删除指定条件
举例说明:
select girl_friend from suzhou where age = 18 and attribute = 'beautiful';
要删除其中 age = 18 这个条件。
为什么要做这个功能呢?难道sql不是写在xml里的么,那直接在xml里面添加if判断不香么?大部分情况下是的,但对于需要可配置的sql,比如报表、大屏、仪表盘这些,就需要将sql存储在数据库里,以便配置修改。而对同一个sql又要求不同的执行人看到不同的结果,因此就需要对一个固定好的sql进行条件的增删,此处就不展开讨论了。
这个功能的实现我写了3种算法,前两种性能会好一些,但不适用于没有格式化的sql。因此就写了一种相对来说适用性更好的算法,只是性能上就会稍稍弱一点。【测试了一个1000字的sql,intel-i7-10700-8核CPU表示执行时间是30ms左右】
(一)主逻辑
基本想法:看下面的第一段代码注释
当然了,里面定义的方法需要在后面慢慢实现
/**
* 主逻辑:
* 1. 匹配关键字
* 2. 找到了关键字 是->3, 否->6
* 3. 判断是否在查询条件语句中: 是->4, 否->5
* 4. 删除该条件,返回删除条件后的sql, 然后继续匹配下一个可能出现的地方 -> 1
* 5. 从当前关键字之后继续查询下一次出现的位置 -> 1
* 6. 返回当前sql
*
* @param sql 当前sql
* @param customKeyword 自关键词
* @param offSet 索引偏移量
* @return 删除了存在关键字之后的sql
*/
public String removeCondition(String sql, String customKeyword, int offSet) {
Matcher matcher = Pattern.compile(customKeyword).matcher(sql);
if (offSet == 0 ? matcher.find() : matcher.find(offSet)) {
int customKeywordStartIndex = matcher.start();
Map<Integer, String> prevKeywordMap = inCondition(sql, customKeywordStartIndex).get(true);
if (prevKeywordMap != null && !prevKeywordMap.isEmpty()) {
return removeCondition(removeExecute(sql, customKeywordStartIndex, prevKeywordMap), customKeyword, offSet);
} else {
return removeCondition(sql, customKeyword, customKeywordStartIndex + 1);
}
} else {
return sql;
}
}
(二)判断当前位置是否在查询条件语句中
0. 简要说明
这里会涉及到两种关键词,先说明一下,以免搞混。
- 我们自己传入的关键词,用来确定需要删除的是哪个查询条件。【下面用“自关键词”来称呼】
- sql本身的关键词,比如 select,from,case,when,join,where等等。【下面用“sql关键词”来称呼】
由于我们输入的自关键词可能存在于sql的各个地方,而我们需要删除的地方仅仅是查询条件语句那里,因此在查询到自关键词所在位置时,需要判断这个地方是否在查询条件语句中。于是就需要定义一个方法来做这件事,就是 inCondition。
但是这件事情做起来并不容易,仅凭一个索引,要怎样知道它在整个sql当中处于什么样的句式呢?是在定义查询字段的位置呢,还是case-when语句中呢,亦或者是在子查询中呢?这就不得不通过sql关键词进行判断,需要找到当前索引位置前面出现的第一个有效的,足够判断出自己所在句式的位置。
当然了,我们可以在判断前方第一个出现的sql关键词之前做另外一件事情 —— 判断当前自关键词的前一个单词或者后一个单词是否是关系运算符。这么做的好处是,只需要判断前一个单词,或者后一个单词,就能排除掉大部分无需判断的情况。
因此就能确定下来一些必须要先做的事情:
i. 定义需要用到的sql关键词
/**
* 关键词列表,可以根据自己的业务场景添加所需判断的关键词
* 为了减少不必要的操作,关键词应当越少越好,但也不能过少从而影响判断
*/
private final List<String> keywordList = new ArrayList<>();
{
keywordList.add("case");
keywordList.add("when");
keywordList.add("then");
keywordList.add("else");
keywordList.add("end");
keywordList.add("join");
keywordList.add("on");
keywordList.add("where");
}
ii. 获取前一个单词和后一个单词
为了使得编码更加简便,这里特地封装了一个从单值map中取值的方法
/**
* 获取单元素map中的key
*
* @param singleMap 单元素Map
* @return 该map中唯一的key
*/
private int keyOfSingleMap(Map<Integer, String> singleMap) {
return singleMap.keySet().iterator().next();
}
/**
* 获取单元素map中的value
*
* @param singleMap 单元素Map
* @return 该map中唯一的value
*/
private String valueOfSingleMap(Map<Integer, String> singleMap) {
return singleMap.get(keyOfSingleMap(singleMap));
}
获取前后的单词
private String prevWord(String str, int currentIndex) {
Map<Integer, String> wordMap = prevWordMap(str, currentIndex);
return valueOfSingleMap(wordMap);
}
private String nextWord(String str, int currentIndex) {
Map<Integer, String> wordMap = nextWordMap(str, currentIndex);
return valueOfSingleMap(wordMap);
}
/**
* 前一个单词【左侧】
*
* @param str 所在语句
* @param currentIndex 当前单词的起始索引【自左向右】
* @return 左侧前一个单词的索引和字符串
*/
private Map<Integer, String> prevWordMap(String str, int currentIndex) {
int firstIndex;
int endIndex;
// 当前索引位置是' ', 则找出前一个单词的结尾,否则找到本单词的开头
Map<Integer, String> wordMap = new HashMap<>(1);
String word;
if (str.charAt(currentIndex) == ' ') {
endIndex = prevWordEndIndex(str, currentIndex);
} else {
firstIndex = currentWordBeginIndex(str, currentIndex);
endIndex = prevWordEndIndex(str, firstIndex - 1);
}
firstIndex = currentWordBeginIndex(str, endIndex);
word = str.substring(firstIndex, endIndex + 1);
wordMap.put(firstIndex, word);
return wordMap;
}
/**
* 下一个单词【右侧】
*
* @param str 所在语句
* @param currentIndex 当前单词的起始索引【自左向右】
* @return 右侧下一个单词的索引和字符串
*/
private Map<Integer, String> nextWordMap(String str, int currentIndex) {
int firstIndex;
int endIndex;
// 当前索引位置是 ‘ ’则找出下一个单词的起始,否则找到本单词起始位置
Map<Integer, String> wordMap = new HashMap<>(1);
String word;
if (str.charAt(currentIndex) == ' ') {
firstIndex = nextWordBeginIndex(str, currentIndex);
} else {
endIndex = currentWordEndIndex(str, currentIndex);
firstIndex = nextWordBeginIndex(str, endIndex + 1);
}
endIndex = currentWordEndIndex(str, firstIndex);
word = str.substring(firstIndex, endIndex + 1);
wordMap.put(firstIndex, word);
return wordMap;
}
这里面写了一些方法,都是用来获取相应索引位置的,直接从方法名看功能,比较简单,不做更多说明了。
private int prevWordEndIndex(String str, int currentIndex) {
for (int i = currentIndex; i >= 0; i--) {
if (str.charAt(i) == ' ') {
continue;
}
if (isEndOfWord(str, i)) {
return i;
}
}
return 0;
}
private int currentWordBeginIndex(String str, int currentIndex) {
for (int i = currentIndex; i >= 0; i--) {
if (isBeginOfWord(str, i)) {
return i;
}
}
return 0;
}
private int nextWordBeginIndex(String str, int currentIndex) {
for (int i = currentIndex; i < str.length(); i++) {
if (str.charAt(i) == ' ') {
continue;
}
if (isBeginOfWord(str, i)) {
return i;
}
}
return 0;
}
private int currentWordEndIndex(String str, int currentIndex) {
for (int i = currentIndex; i < str.length(); i++) {
if (isEndOfWord(str, i)) {
return i;
}
}
return 0;
}
/**
* 当前索引指向单词的最后,但不超过该单词
*/
private boolean isEndOfWord(String str, int index) {
if (index == str.length() - 1) {
return true;
}
return (str.charAt(index + 1) == ' ' || str.charAt(index + 1) == ')') && str.charAt(index) != ' ';
}
/**
* 当前索引指向单词的开始,但不超过该单词
*/
private boolean isBeginOfWord(String str, int index) {
if (index == 0) {
return true;
}
return (str.charAt(index - 1) == ' ' || str.charAt(index - 1) == '(') && str.charAt(index) != ' ';
}
iii.获取前一个sql关键词
private Map<Integer, String> prevKeywordMap(String sql, int currentIndex) {
Map<Integer, String> wordMap = prevWordMap(sql, currentIndex);
Integer index = wordMap.keySet().iterator().next();
if (!keywordList.contains(wordMap.get(index).toLowerCase())) {
return prevKeywordMap(sql, index);
} else {
return wordMap;
}
}
private Map<Integer, String> nextKeywordMap(String sql, int currentIndex) {
Map<Integer, String> wordMap = nextWordMap(sql, currentIndex);
Integer index = wordMap.keySet().iterator().next();
if (!keywordList.contains(wordMap.get(index).toLowerCase())) {
return nextKeywordMap(sql, index);
} else {
return wordMap;
}
}
iv. 判断当前词是否是关系运算符
/**
* 当前”单词“是否是关系运算符
* 其他复杂的情况,包括大小写问题可自行扩展
*/
private boolean isRelationalOperator(String word) {
return word.contains("=")
|| word.contains(">")
|| word.contains("<")
|| word.contains("between")
|| word.contains("and")
|| word.contains("or");
}
v. 判断自关键词是否在查询条件语句中
/**
* 前一个单词或后一个单词是关系运算符则判定当前索引在条件语句或者case-when语句中
* 通过前面的第一个关键词,排除其在case-when语句中的情况
*/
private Map<Boolean, Map<Integer, String>> inCondition(String sql, int currentIndex) {
Map<Boolean, Map<Integer, String>> result = new HashMap<>(1);
Map<Integer, String> prevKeywordMap = prevKeywordMap(sql, currentIndex);
String prevKeyword = valueOfSingleMap(prevKeywordMap);
if ((isRelationalOperator(prevWord(sql, currentIndex)) || isRelationalOperator(nextWord(sql, currentIndex)))
&& ("join".equalsIgnoreCase(prevKeyword) || "on".equalsIgnoreCase(prevKeyword) || "where".equalsIgnoreCase(prevKeyword))) {
result.put(true, prevKeywordMap);
}
return result;
}
(三)执行删除操作
方法定义:
/**
* 已知关键字所在位置是在条件中,要删除该关键字所在的查询条件
*
* @param sql 给定sql
* @param customKeywordIndex 自关键词索引
* @param prevKeywordMap sql关键词Map
* @return 删除了当前关键字索引所在的条件后的sql
*/
private String removeExecute(String sql, int customKeywordIndex, Map<Integer, String> prevKeywordMap) {
List<Integer> indexList = currentConditionInfo(sql, customKeywordIndex, prevKeywordMap);
StringBuilder sqlBuilder = new StringBuilder(sql);
StringBuilder newSql = sqlBuilder.delete(indexList.get(0), indexList.get(1));
return newSql.toString();
}
到这一步,那就表示我们已经知道了当前位置必然在查询条件语句中。而且参数中的 sqlKeyword 还可以指明是在 where 语句中还是在 join on 语句中。但还有一件事情是我们不知道的:该查询条件的前面有没有条件?后面有没有条件?
因此就需要对这种情况进行讨论:
前面有条件 | 后面有条件 | 操作方案 |
---|---|---|
0 | 0 | (where语句中)删除整个条件,(join语句中)不操作 |
0 | 1 | 删除当前条件语句+后面的 “and” |
1 | 0 | 从前一个“and”开始删除当前条件语句 |
1 | 1 | 从前一个“and”开始删除当前条件语句 |
归纳:只要前面出现and,就从前面的and开始删除,直到该条件结束。
确定当前条件语句的起末位置
实现如下:
/**
* 获取当前位置所在条件语句的信息
*
* @param sql 给定sql
* @param customKeywordIndex 自关键词索引
* @param prevKeywordMap sql关键词Map
* @return indexList: [0]——表示起始索引 [1]——表示结束索引
*/
private List<Integer> currentConditionInfo(String sql, int customKeywordIndex, Map<Integer, String> prevKeywordMap){
List<Integer> indexList = new ArrayList<>(2);
int keyOfSingleMap = keyOfSingleMap(prevKeywordMap);
String sqlKeyword = prevKeywordMap.get(keyOfSingleMap);
int prevAndIndex = prevAndIndex(sql, customKeywordIndex, sqlKeyword);
int nextAndIndex = nextAndIndex(sql, customKeywordIndex, sqlKeyword);
// 根据前一个and和后一个and的索引【它们不一定都存在】判断出当前条件语句的起末位置
if (prevAndIndex != 0) {
indexList.add(0, prevAndIndex);
if (nextAndIndex != 0) {
indexList.add(1, nextAndIndex);
} else {
// 在where语句中,直接到sql结尾;否则查找下一个sql关键词
if ("where".equalsIgnoreCase(sqlKeyword)) {
indexList.add(1, sql.length());
} else {
Map<Integer, String> nextKeywordMap = nextKeywordMap(sql, customKeywordIndex);
int nextKeywordIndex = keyOfSingleMap(nextKeywordMap);
indexList.add(1, nextKeywordIndex);
}
}
} else {
// 当前条件语句是查询语句中第一个条件
if (nextAndIndex != 0) {
// 改条件语句后面还有其他条件语句
indexList.add(0, keyOfSingleMap + sqlKeyword.length());
indexList.add(1, nextAndIndex + 3);
} else {
// 该条件语句是唯一的条件语句,where语句整个删除,join语句不做操作
if ("where".equalsIgnoreCase(sqlKeyword)) {
indexList.add(0, keyOfSingleMap);
indexList.add(1, sql.length());
}
}
}
return indexList;
}
查找前后and关键词
/**
* 获取当前条件语句中前一个 and 关键词的索引
*/
private int prevAndIndex(String sql, int customKeywordIndex, String sqlKeyword) {
// 往前查询,直到sql关键词之处。若存在”and“,说明这不是第一个条件语句,否则这是第一个条件语句
Map<Integer, String> prevWordMap = prevWordMap(sql, customKeywordIndex);
int prevWordIndex = prevWordMap.keySet().iterator().next();
int prevAndIndex = 0;
while (!sqlKeyword.equalsIgnoreCase(prevWordMap.get(prevWordIndex))) {
if ("and".equalsIgnoreCase(prevWordMap.get(prevWordIndex))) {
prevAndIndex = prevWordIndex;
break;
} else {
prevWordMap = prevWordMap(sql, prevWordIndex);
prevWordIndex = prevWordMap.keySet().iterator().next();
}
}
return prevAndIndex;
}
/**
* 获取当前条件语句中下一个 and 关键词的索引
*/
private int nextAndIndex(String sql, int customKeywordIndex, String sqlKeyword) {
// 往后查询,如果当前是在join语句中,则设定循环截至到where关键词;否则循环截至到最后。判断这里面有没有出现“and”
Map<Integer, String> nextWordMap = nextWordMap(sql, customKeywordIndex);
int nextWordIndex = keyOfSingleMap(nextWordMap);
int nextAndIndex = 0;
while (nextWordIndex < sql.length()) {
if ("and".equalsIgnoreCase(nextWordMap.get(nextWordIndex))) {
nextAndIndex = nextWordIndex;
break;
}else{
nextWordMap = nextWordMap(sql, nextWordIndex);
nextWordIndex = keyOfSingleMap(nextWordMap);
}
// join语句中则以下一个where关键词为截止点
if ("on".equalsIgnoreCase(sqlKeyword) && "where".equalsIgnoreCase(nextWordMap.get(nextWordIndex))) {
break;
}
}
return nextAndIndex;
}
三、小结
总体来说,该工具类的实现是通过对sql语句的一般词法进行归纳后做的字符串解析。
先定下总体思路,然后从细节上慢慢实现。我相信,只要是我一眼就能知道怎么去修改这个sql,那么用程序必定能将它实现。只是实现的过程没有想象中的容易就是了。
整个代码放在了我的简书,方便以后copy:从sql中提取表名称,删除sql中的某个查询条件