峰之博纹 - Pelephone个人博客

单句SQL语句的解析方案。

最近想改写一下数据库交互的底层代码,搞来两篇sql解析的方案文章.不管这么多,先转了再说...

数据是程序处理的主要内容,它一般存储在关系型数据库中,要操作它们最终必须要通过SQL语句来完成,因此,解读分析和处理SQL语句成为程序员的基本工作内容之一,当然有时这项任务是比较乏味的,如果让计算机来完成一些基本的分析解读工作如找出SQL语句涉及了哪些表,字段和条件等,可以帮助程序员解放出部分精力,投入到更有挑战性和复杂性的任务中去,本文将就如何解析单句SQL语句提出自己的解决方案和大家探讨,希望大家不吝批评指正。

  首先说明以下单句SQL的范畴,它是指不存在嵌套的SQL语句,包括Select,insert,delete,update四大类型(具体的解析子类还有一种insert select类型),其中以select最为复杂,下面将以它为例。另关于嵌套SQL尤其是多重嵌套SQL的分析似乎比较复杂,我一时没想出好的解决方案,如果您知道请不吝赐教。

  1.关于SQL语句的预处理。

  在对sql语句进行分析之前,有必要对它进行一些预处理,这样能减轻不少后面编程的负担。

  预处理的主要工作是消除SQL语句前后的空白,将其中的连续空白字符(包括空格,TAB和回车换行)替换成单个空格;将sql语句全变成小写形式(或大写形式);在SQL语句的尾后加上结束符号,至于为什么加,这里先买个关子。具体的语句如下:

 sql=sql.trim();
 sql=sql.toLowerCase();
 sql=sql.replaceAll("s+", " ");
 sql=""+sql+" ENDOFSQL";

  2.将SQL语句分离成片段。

  经过第一步的工作,一个多行的,存在大小写混杂的SQL语句已经变成了单行的小写SQL语句,接下来我们需要把整句分离成更小的片段。以Select语句为例,其中可能存在有select子句部分,from子句部分,where子句部分,group by子句和order by子句等,如果能成功的把整句分离成这些子句,我们的分析工作又前进了一步。先让我们看看下面的SQL示例:

 
关键字: select 子表树 企业管理器 DBCC PAGE REVERT

单句SQL语句的解析方案。

 

  select c1,c2,c3 from t1,t2 where condi3=3 or condi4=5 order by o1,o2

  通过观察我们可以发现,select子句是select c1,c2,c3 from,它的起始标志是select,结束标志是from;from子句是from t1,t2 where,它的起始标志是from,结束标志是where;where子句是where condi3=3 or condi4=5,它的起始标志是where,结束标志是order by;order by子句是order by o1,o2其起始标志是order by,刚才我们在整句SQL尾后加上了" ENDOFSQL"字样,因此,order by子句的结束标志是" ENDOFSQL"。

  这个分析给我们解析SQL语句提供了一个思路,如果我们能找到各个子句的前后标志,在正则表达式的帮助下我们就可以轻松的获得每一种子句,下面给出一个找到from子句的完整正则表达式:

  "(from)(.+)( where | on | having | groups+by | orders+by | ENDOFSQL)"

  这句正则表示式让程序到整句SQL中查找符合这样条件的文本单元:它以from开头,结束标志是where,on,having,group by,order by或语句结束中间的一个,开始标志和结束标志之间可以是任何字符。这样,from子句的各种情况就都囊括进这个正则表达式了,它能找到以下类型的各种form子句:

  from .... where

  from .... on

  from .... having

  from .... group by

  from .... order by

  from .... ENDOFSQL(这个ENDOFSQL是预处理时加上的,如果用$符号会给程序造成麻烦)

  3.找到片段中的各个部分。

  有了表示片段的正则表达式,找到片段后从中分离出片段起始标志start,片段主体body和片段结束标志end就很容易了,请见代码:

Pattern pattern=Pattern.compile(segmentRegExp,Pattern.CASE_INSENSITIVE);
 for(int i=0;i<=sql.length();i++){
  String shortSql=sql.substring(0, i);
  //System.out.println(shortSql);
   Matcher matcher=pattern.matcher(shortSql);
   while(matcher.find()){
    start=matcher.group(1);// 片段起始标志start
  body=matcher.group(2);// 片段主体body
  end=matcher.group(3);// 片段结束标志end
  parseBody();
  return;
   }
 }

 
关键字: select 子表树 企业管理器 DBCC PAGE REVERT

单句SQL语句的解析方案。

 

  这段代码为什么要逐渐从SQL开头开始截取不断增长的SQL语句进行分析而不是直接对整个SQL进行查找呢?原因是表示子句的正则表达式比较贪婪,它会竭力向后寻找,比如说SQL语句是这样写的:

  select .... from .... where .... order by ....

  那么用"(from)(.+)( where | on | having | groups+by | orders+by | ENDOFSQL)"进行查找得到from子句不是

  from .... where

  而是

  from .... where .... order by

  这当然不是我们想要的结果,因此采取了从SQL开头开始截取不断增长的SQL语句进行分析,找到了from .... where部分就不用继续往下找了,当然这在效率上有降低,但一些效率的付出相对于正确的结果来说是值得的。

  4.将片段主体部分劈分开来

  还是拿from子句做例子,得到它以后我们希望继续进行分析,最终得到from子句的表,这部分工作比较简单,使用特定的标志对body进行查找劈分即可,from子句的劈分标志较多,用正则表达式写出来是“(,|s+lefts+joins+|s+rights+joins+|s+inners+joins+)”,各种情况都要涉及到。其实大多数子句主体部分的劈分标志都是逗号,用正则表达式写出来都比from子句的简单。

  劈分的代码如下,每个分隔符之间的小部分放在链表中:

List<String> ls=new ArrayList<String>(); 
 Pattern p = Pattern.compile(bodySplitPattern,Pattern.CASE_INSENSITIVE);// bodySplitPattern就是劈分的正则表达式
 // 先清除掉前后空格
 body=body.trim();
 Matcher m = p.matcher(body);
 StringBuffer sb = new StringBuffer();
 boolean result = m.find();
 while (result) {
  m.appendReplacement(sb, m.group(0) + Crlf);
  result = m.find();
 }
 m.appendTail(sb);
 // 再按空行断行
 String[] arr=sb.toString().split("[n]+");
 int arrLength=arr.length;
 for(int i=0;i<arrLength;i++){
  String temp=FourSpace+arr[i];
  if(i!=arrLength-1){
  temp=temp+Crlf;
  }
  ls.add(temp);
 }

 
关键字: select 子表树 企业管理器 DBCC PAGE REVERT

单句SQL语句的解析方案。

 

  之所以不直接使用String的split方法是因为分隔符存在多种形式,使用split方法后将无从知晓以前的劈分符是什么,比如 where c1=1 and c2=2 or c3=3,如果使用(and|or)做劈分符再用split方法,那么再还原SQL语句是将不可能知道原先的分隔符是and还是or。

  5.还原整个SQL语句。

  分析完SQL语句后,最终是要以清晰完整的形式将SQL还原出来,这一步的工作主要是将各个子句又重新组合起来,如果分析的语句是

  select c1,c2,c3 from  t1,t2, t3 where condi1=5 and condi6=6 or condi7=7 order by g1,g2

  解析后的的Sql为:

select
  c1,
  c2,
  c3
from
  t1,
  t2,
  t3
where
  condi1=5 and
  condi6=6 or
  condi7=7
order by
  g1,
  g2
到这里,我对单句SQL语句进行分析的基本思路都写完了,下面是完整的代码示例:

  工具类SqlParserUtil,这是进行SQL解析的入口

public class SqlParserUtil{
public static String getParsedSql(String sql){
 sql=sql.trim();
 sql=sql.toLowerCase();
 sql=sql.replaceAll("s+", " ");
 sql=""+sql+" ENDOFSQL";
 return SingleSqlParserFactory.generateParser(sql).getParsedSql();
}
}

  单句Sql解析器基类:

package com.sitinspring.common.sqlparser.single;
import java.util.ArrayList;
import java.util.List;
import com.sitinspring.common.sqlparser.SqlSegment;
/** *//**
* 单句Sql解析器,单句即非嵌套的意思
* @author 何杨(heyang78@gmail.com)
*
* @since 2009-2-2 下午03:01:06
* @version 1.00
*/
public abstract class BaseSingleSqlParser{
/** *//**
 * 原始Sql语句
 */
protected String originalSql;
/** *//**
 * Sql语句片段
 */
protected List<SqlSegment> segments;
/** *//**
 * 构造函数,传入原始Sql语句,进行劈分。
 * @param originalSql
 */
public BaseSingleSqlParser(String originalSql){
 this.originalSql=originalSql;
 segments=new ArrayList<SqlSegment>();
 initializeSegments();
 splitSql2Segment();
}
/** *//**
 * 初始化segments,强制子类实现
 *
 */
protected abstract void initializeSegments();
/** *//**
 * 将originalSql劈分成一个个片段
 *
 */
protected void splitSql2Segment() {
 for(SqlSegment sqlSegment:segments){
  sqlSegment.parse(originalSql);
 } 
}
/** *//**
 * 得到解析完毕的Sql语句
 * @return
 */
public String getParsedSql() {
 StringBuffer sb=new StringBuffer();
 for(SqlSegment sqlSegment:segments){
  sb.append(sqlSegment.getParsedSqlSegment()+"n");
 } 
 String retval=sb.toString().replaceAll("n+", "n"); 
 return retval;
}
}

 
关键字: select 子表树 企业管理器 DBCC PAGE REVERT

单句SQL语句的解析方案。

 

  下面是BaseSingleSqlParser的五种子类:

package com.sitinspring.common.sqlparser.single;
import com.sitinspring.common.sqlparser.SqlSegment;
/** *//**
*
* 单句删除语句解析器
* @author 何杨(heyang78@gmail.com)
*
* @since 2009年2月3日8:58:48
* @version 1.00
*/
public class DeleteSqlParser extends BaseSingleSqlParser{
public DeleteSqlParser(String originalSql) {
 super(originalSql);
}
@Override
protected void initializeSegments() { 
 segments.add(new SqlSegment("(delete from)(.+)( where | ENDOFSQL)","[,]"));
 segments.add(new SqlSegment("(where)(.+)( ENDOFSQL)","(and|or)"));
}
}
package com.sitinspring.common.sqlparser.single;
import com.sitinspring.common.sqlparser.SqlSegment;
/** *//**
*
* 单句查询插入语句解析器
* @author 何杨(heyang78@gmail.com)
*
* @since 2009年2月3日9:41:23
* @version 1.00
*/
public class InsertSelectSqlParser extends BaseSingleSqlParser{
public InsertSelectSqlParser(String originalSql) {
 super(originalSql);
}
@Override
protected void initializeSegments() { 
 segments.add(new SqlSegment("(insert into)(.+)( select )","[,]"));
 segments.add(new SqlSegment("(select)(.+)(from)","[,]"));
 segments.add(new SqlSegment("(from)(.+)( where | on | having | groups+by | orders+by | ENDOFSQL)","(,|s+lefts+joins+|s+rights+joins+|s+inners+joins+)"));
 segments.add(new SqlSegment("(where|on|having)(.+)( groups+by | orders+by | ENDOFSQL)","(and|or)"));
 segments.add(new SqlSegment("(groups+by)(.+)( orders+by| ENDOFSQL)","[,]"));
 segments.add(new SqlSegment("(orders+by)(.+)( ENDOFSQL)","[,]"));
}
}
package com.sitinspring.common.sqlparser.single;
import com.sitinspring.common.sqlparser.SqlSegment;
/** *//**
*
* 单句插入语句解析器
* @author 何杨(heyang78@gmail.com)
*
* @since 2009年2月3日9:16:44
* @version 1.00
*/
public class InsertSqlParser extends BaseSingleSqlParser{
public InsertSqlParser(String originalSql) {
 super(originalSql);
}
@Override
protected void initializeSegments() { 
 segments.add(new SqlSegment("(insert into)(.+)([(])","[,]"));
 segments.add(new SqlSegment("([(])(.+)( [)] values )","[,]"));
 segments.add(new SqlSegment("([)] values [(])(.+)( [)])","[,]")); 
}
@Override
public String getParsedSql() {
 String retval=super.getParsedSql();
 retval=retval+")";
 return retval;
}
}
package com.sitinspring.common.sqlparser.single;
import com.sitinspring.common.sqlparser.SqlSegment;
/** *//**
*
* 单句查询语句解析器
* @author 何杨(heyang78@gmail.com)
*
* @since 2009-2-2 下午03:30:54
* @version 1.00
*/
public class SelectSqlParser extends BaseSingleSqlParser{
public SelectSqlParser(String originalSql) {
 super(originalSql);
}
@Override
protected void initializeSegments() { 
 segments.add(new SqlSegment("(select)(.+)(from)","[,]"));
 segments.add(new SqlSegment("(from)(.+)( where | on | having | groups+by | orders+by | ENDOFSQL)","(,|s+lefts+joins+|s+rights+joins+|s+inners+joins+)"));
 segments.add(new SqlSegment("(where|on|having)(.+)( groups+by | orders+by | ENDOFSQL)","(and|or)"));
 segments.add(new SqlSegment("(groups+by)(.+)( orders+by| ENDOFSQL)","[,]"));
 segments.add(new SqlSegment("(orders+by)(.+)( ENDOFSQL)","[,]"));
}
}
package com.sitinspring.common.sqlparser.single;
import com.sitinspring.common.sqlparser.SqlSegment;
/** *//**
*
* 单句更新语句解析器
* @author 何杨(heyang78@gmail.com)
*
* @since 2009年2月3日9:08:46
* @version 1.00
*/
public class UpdateSqlParser extends BaseSingleSqlParser{
public UpdateSqlParser(String originalSql) {
 super(originalSql);
}
@Override
protected void initializeSegments() { 
 segments.add(new SqlSegment("(update)(.+)(set)","[,]"));
 segments.add(new SqlSegment("(set)(.+)( where | ENDOFSQL)","[,]"));
 segments.add(new SqlSegment("(where)(.+)( ENDOFSQL)","(and|or)"));
}
}
  
下面是用于找到具体子类分析器的工厂类:
package com.sitinspring.common.sqlparser.single;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.sitinspring.common.sqlparser.exception.NoSqlParserException;
/** *//**
* 单句Sql解析器制造工厂
* @author 何杨(heyang78@gmail.com)
*
* @since 2009-2-3 上午09:45:49
* @version 1.00
*/
public class SingleSqlParserFactory{
public static BaseSingleSqlParser generateParser(String sql){
 if(contains(sql,"(insert into)(.+)(select)(.+)(from)(.+)")){
  return new InsertSelectSqlParser(sql);
 }
 else if(contains(sql,"(select)(.+)(from)(.+)")){
  return new SelectSqlParser(sql);
 }
 else if(contains(sql,"(delete from)(.+)")){
  return new DeleteSqlParser(sql);
 }
 else if(contains(sql,"(update)(.+)(set)(.+)")){
  return new UpdateSqlParser(sql);
 }
 else if(contains(sql,"(insert into)(.+)(values)(.+)")){
  return new InsertSqlParser(sql);
 }
 //sql=sql.replaceAll("ENDSQL", "");
 throw new NoSqlParserException(sql.replaceAll("ENDOFSQL", ""));
}
/** *//**
 * 看word是否在lineText中存在,支持正则表达式
 * @param sql:要解析的sql语句
 * @param regExp:正则表达式
 * @return
 */
private static boolean contains(String sql,String regExp){
 Pattern pattern=Pattern.compile(regExp,Pattern.CASE_INSENSITIVE);
 Matcher matcher=pattern.matcher(sql);
 return matcher.find();
}
}
  
最后是表示子句的SqlSegment类:
package com.sitinspring.common.sqlparser;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** *//**
* Sql语句片段
*
* @author 何杨(heyang78@gmail.com)
*
* @since 2009-2-2 下午03:10:29
* @version 1.00
*/
public class SqlSegment{
private static final String Crlf = "n";
private static final String FourSpace = "  ";
/** *//**
 * Sql语句片段开头部分
 */
private String start;
/** *//**
 * Sql语句片段中间部分
 */
private String body;
/** *//**
 * Sql语句片段结束部分
 */
private String end;
/** *//**
 * 用于分割中间部分的正则表达式
 */
private String bodySplitPattern;
/** *//**
 * 表示片段的正则表达式
 */
private String segmentRegExp;
/** *//**
 * 分割后的Body小片段
 */
private List<String> bodyPieces;
/** *//**
 * 构造函数
 * @param segmentRegExp 表示这个Sql片段的正则表达式
 * @param bodySplitPattern 用于分割body的正则表达式
 */
public SqlSegment(String segmentRegExp,String bodySplitPattern){
 start="";
 body="";
 end=""; 
 this.segmentRegExp=segmentRegExp;
 this.bodySplitPattern=bodySplitPattern;
 this.bodyPieces=new ArrayList<String>();
}
/** *//**
 * 从sql中查找符合segmentRegExp的部分,并赋值到start,body,end等三个属性中
 * @param sql
 */
public void parse(String sql){
 Pattern pattern=Pattern.compile(segmentRegExp,Pattern.CASE_INSENSITIVE);
 for(int i=0;i<=sql.length();i++){
  String shortSql=sql.substring(0, i);
  //System.out.println(shortSql);
   Matcher matcher=pattern.matcher(shortSql);
   while(matcher.find()){
    start=matcher.group(1);
  body=matcher.group(2);
  end=matcher.group(3);
  parseBody();
  return;
   }
 }
}
/** *//**
 * 解析body部分
 *
 */
private void parseBody(){
 List<String> ls=new ArrayList<String>(); 
 Pattern p = Pattern.compile(bodySplitPattern,Pattern.CASE_INSENSITIVE);
 // 先清除掉前后空格
 body=body.trim();
 Matcher m = p.matcher(body);
 StringBuffer sb = new StringBuffer();
 boolean result = m.find();
 while (result) {
  m.appendReplacement(sb, m.group(0) + Crlf);
  result = m.find();
 }
 m.appendTail(sb);
 // 再按空格断行
 String[] arr=sb.toString().split("[n]+");
 int arrLength=arr.length;
 for(int i=0;i<arrLength;i++){
  String temp=FourSpace+arr[i];
  if(i!=arrLength-1){
  temp=temp+Crlf;
  }
  ls.add(temp);
 }
 bodyPieces=ls;
}
/** *//**
 * 取得解析好的Sql片段
 * @return
 */
public String getParsedSqlSegment(){
 StringBuffer sb=new StringBuffer();
 sb.append(start+Crlf);
 for(String piece:bodyPieces){
  sb.append(piece+Crlf);
 } 
 return sb.toString();
}
public String getBody() {
 return body;
}
public void setBody(String body) {
 this.body = body;
}
public String getEnd() {
 return end;
}
public void setEnd(String end) {
 this.end = end;
}
public String getStart() {
 return start;
}
public void setStart(String start) {
 this.start = start;
}
}

posted @ 2011-11-27 10:27  Pelephone  阅读(16271)  评论(0编辑  收藏  举报