cms查詢系統(二)json形式參數的設計與解析

#1 前言git

本篇文章主要來講明下代碼模塊的設計。像咱們這種菜鳥級別,只有平時多讀讀源碼,多研究和探討其中的設計纔可能提高本身,寫出高質量的代碼。sql

沒有最好的設計,只有更好的設計,因此在發表我本身的愚見的同時,但願小夥伴們相互探討更好的設計,有探討纔有更大的進步。mongodb

#2 題目及分析json

咱們維護了一個數據中心,對外提供查詢API,如何能讓用戶隨意的添加查詢條件,而不用修改後臺的查詢代碼呢?用戶如何配置查詢條件,從而達到以下的sql效果呢?:ide

a.name='lg' or b.age>12
b.id in (12,34,45)
c.updateTime>'2015-3-28' and (b.id=2 or d.age<23)
e.age>f.age

##2.1 查詢參數的傳遞方式ui

咱們做爲API設計者,該如何讓用戶方便的傳遞他們任意的查詢需求呢?這是咱們要思考的地方。this

目前來看比較好的方式莫過於:用戶經過json來表達他們的查詢需求。.net

##2.2 查詢的本質分析設計

從上面的查詢來看,咱們能夠總結出來查詢條件無非就是某個字段知足什麼樣的條件。這裏有三個對象:unix

  • 查詢的字段 如 b.age
  • 條件值 如 12
  • 怎樣知足條件值 如 >

##2.3 查詢的配置分析

這樣咱們就能夠清晰明瞭了,一個查詢條件無非就是三個內容,因此能夠以下配置:

{
	"columns":"b.age",
	"oper":">",
	"value":12
}

很顯然,上面的確很麻煩,咱們無非是要表達這三個內容,因此就要簡化配置:

{
	"b.age":12,
	"oper":">"
}

仍是不夠簡化,如何把操做符 > 也放置進去呢?以下

{
	"b.age@>":12
}

這樣咱們就能夠把三個對象表達清楚了,將查詢的字段和操做符合並起來做爲key,並使用分隔符@分割二者,條件值做爲value。這樣就作到了,很是簡化的查詢配置。

接下來又面臨一個問題,如何表達查詢條件之間的and or 關係呢?即如何表達下面的內容呢?

c.age>14 and (b.id=2 or d.age<23)

借鑑mongodb的查詢方案,能夠以下配置:

{
	"c.age@>":14,
	"$or":{
		"b.id@=":2,
		"d.age@<":23
	}
}

經過配置一個$or做爲key代表裏面的幾個查詢條件是or的關係,若是是$and代表裏面的查詢條件之間是and的關係,外層默認是and的關係。

同時咱們再回顧下,mongodb所做出的查詢設計,也是經過用戶配置json形式來表達查詢意圖,可是咱們來看下它是如何查詢

a.age>12 對應的mongodb的查詢方式爲:
{
	a.age : {
		$gt : 22
	}
}

咱們的查詢方式是

{
	"a.age@>":22
}

雖然看似咱們的更加簡單,mongodb的更加繁瑣,主要是mongodb認爲對於一個字段,能夠有多個查詢條件的,爲了支持更加複雜的查詢,以下:

{	
	a.age : {
		$lt :24,
		$gt : 17
	}
}

然而咱們也能夠對此進行拆分,以下,一樣知足:

{
	"a.age@>":17,
	"a.age@<":24,
}

各有各的好處和缺點,我就是我,顏色不同的煙火。哈哈。。。

#3 代碼設計與實現

##3.1 解析器接口的設計

對題目進行分析完了以後,就要考慮如何實現這樣的json配置到sql的轉化。實現起來不難,最重要的是如何作出一個高擴展性的實現?

再來看下下面的例子:

{
	"a.name@=":"lg",
	"b.age@>":12,
	"c.id@in":[12,13,14],
	"d.time[@time](http://my.oschina.net/u/126678)>":"2015-3-1"
}

其實就是針對每一個自定義的操做符進行相應的處理,因此就有了解析器接口:

public interface SqlParamsParser {

	//這裏表示該解析器是否支持對應的操做符
	public boolean support(String oper);

	public String getParams(String key,Object value,String oper);
	
	public SqlParamsParseItemResult getParamsResult(String key,Object value,String oper);

}

其中SqlParamsParseItemResult,則是把解析後的結果分別存儲起來,而不是直接拼接成一個字符串,主要爲了直接拼接字符串式的sql注入,它的內容以下:

public class SqlParamsParseItemResult {
	private String key;
	private String oper;
	private Object value;
}

上面的key oper value 則是解析後的內容。下面舉例說明

以"b.age@>":12 爲例,其中getParams方法中的 key就是b.age, value就是12, oper就是> 而這個方法的返回的字符串結果爲:

b.age>12

返回的SqlParamsParseItemResult存儲的內容爲分別爲 key=b.age ; oper=> ; value=12

以"c.id@in":[12,13,14]爲例,其中getParams方法中的 key就是c.id,value就是一個List集合,oper就是in ,這個方法的返回結果爲:

c.id in (12,13,14)

返回的SqlParamsParseItemResult存儲的內容爲分別爲 key=c.id ; oper=in ; value=(12,13,14)

以"d.time@time>":"2015-3-1"爲例,其中getParams方法中的 key就是c.id,value就是一個List集合,oper就是in,這個方法的返回結果爲:

unix_timestamp(d.time) > 1425139200     (2015-3-1對應的秒數)

返回的SqlParamsParseItemResult存儲的內容爲分別爲 key=unix_timestamp(d.time) ; oper=> ; value=1425139200

##3.2 解析器接口的抽象類

解析器有不少相同的地方,這就須要咱們進行抽象,抽出共性部分,留給子類去實現不一樣的部分。因此有了抽象類AbstractSqlParamsParser

有哪些共性部分和非共性部分呢?

  • 共性部分: 就是support方法。每一個解析器支持某幾種操做符,因此判斷該解析器是否支持當前的操做符的邏輯是共同的,因此以下:

    public abstract class AbstractSqlParamsParser implements SqlParamsParser{
    
    	private String[] opers;	
    	private boolean ignoreCase=true;
    
    	protected void setOpers(String[] opers){
    		this.opers=opers;
    	}	
    	protected void setIgnoreCase(boolean ignoreCase){
    		this.ignoreCase=ignoreCase;
    	}
    	@Override
    	public boolean support(String oper) {
    		if(opers!=null && oper!=null){
    			for(String operItem:opers){
    				if(ignoreCase){
    					operItem=operItem.toLowerCase();
    					oper=oper.toLowerCase();
    				}
    				if(operItem.equals(oper)){
    					return true;
    				}
    			}
    		}
    		return false;
    	}
    }

    opers屬性表示當前解析器所支持的全部操做符。ignoreCase表示在匹配操做符的時候是否忽略大小寫。這兩個屬性都設置成private,而後對子類開放了protected類型的set方法,用於子類來設置這兩個屬性。

  • 非共性部分:留出了doParams方法供子類來具體實現

    @Override
    public SqlParamsParseItemResult getParamsResult(String key, Object value,
    		String oper) {
    	return doParams(key, processStringValue(value), oper);
    }
    
    protected abstract SqlParamsParseItemResult doParams(String key, Object value, String oper);

##3.3 解析器接口的實現類

目前內置了幾個經常使用的解析器實現,類圖以下: SqlParamsParser接口類圖

以TimeSqlParamsParser爲例來簡單說明下:

它主要是用於解析以下形式的:

{
	"d.time@time>":"2015-3-1"
}

最終想達到的效果是:

unix_timestamp(d.time) > 1425139200

它的解析過程以下:

/**
 * 以d.time@time>'2015-3-1'爲例
 * 初始參數 key=d.time; value='2015-3-1'; oper=time>
 * 解析後的key=unix_timestamp(d.time); value=1425139200('2015-3-1'對應的秒數); oper=>
 */
@Override
protected SqlParamsParseItemResult doParams(String key, Object value, String oper) {
	String timeKey="unix_timestamp("+key+")";
	String realOper=oper.substring(4+fullTimeFlag.length());
	if(value instanceof String){
		String tmp=(String)value;
		Assert.isLarger(tmp.length(),2,"時間參數不合法");
		//默認進行了字符串處理,即加上了'',如今要去掉,而後解析成時間的秒數
		value=tmp.substring(1,tmp.length()-1);
		try {
			SimpleDateFormat format=new SimpleDateFormat(timeFormat);
			Date date=format.parse((String)value);
			value=date.getTime()/1000;
		} catch (ParseException e) {
			e.printStackTrace();
			throw new IllegalArgumentException("timeFormat爲"+timeFormat+";value="+value+";出現瞭解析異常");
		}
	}else{
		Assert.isInstanceof(value,Number.class,"時間參數必須爲時間的秒數");
	}
	return new SqlParamsParseItemResult(timeKey,realOper,value);
}

解析過程其實就是對key value oper 進行了不一樣程度的轉換。

同時TimeSqlParamsParser還支持其餘時間形式的解析,如"2015-3-1 12:23:12",只需以下方式建立一個解析器:

new TimeSqlParamsParser("yyyy-MM-dd HH:mm:ss","full_")

而後他就可以解析下面的形式:

{
	"d.time@full_time>":"2015-3-1 12:23:12"
}

同時又能保留原有的形式,二者互不干擾。

#4 DefaultSqlParamsHandler使用解析器

有了解析器的一系列實現,下面就須要一個綜合的類來使用這些解析器。這就是DefaultSqlParamsHandler:

##4.1 註冊使用解析器

public class DefaultSqlParamsHandler {

private List<SqlParamsParser> sqlParamsParsers;

public DefaultSqlParamsHandler(){
	sqlParamsParsers=new ArrayList<SqlParamsParser>();
	sqlParamsParsers.add(new DefaultSqlParamsParser());
	sqlParamsParsers.add(new InSqlParamsParser());
	sqlParamsParsers.add(new TimeSqlParamsParser());
	sqlParamsParsers.add(new TimeSqlParamsParser("yyyy-MM-dd HH:mm:ss","full_"));
	sqlParamsParsers.add(new DefaultColumnSqlParamsParser());
}

內部已經註冊了幾個解析器。同時須要對外留出註冊自定義解析器的方法:

public void registerSqlParamsHandler(SqlParamsParser sqlParamsParser){
	if(sqlParamsParser!=null){
		sqlParamsParsers.add(sqlParamsParser);
	}
}

public void registerSqlParamsHandler(List<SqlParamsParser> sqlParamsParsers){
	if(sqlParamsParsers!=null){
		for(SqlParamsParser sqlParamsParser:sqlParamsParsers){
			registerSqlParamsHandler(sqlParamsParser);
		}
	}
}

##4.2 解析過程

這個過程不只須要使用已經註冊的解析器來解析,還包含對解析條件之間的and or 關係的遞歸處理。代碼以下,再也不詳細說明:

private SqlParamsParseResult getSqlWhereParamsResultByAndOr(Map<String,Object> params,String andOr,
		boolean isPlaceHolder,SqlParamsParseResult sqlParamsParseResult){
	if(params!=null){
		String andOrDelititer=" "+andOr+" ";
		for(String key:params.keySet()){
			Object value=params.get(key);
			if(value instanceof Map){
				//這裏須要進行遞歸處理嵌套的查詢條件
				SqlParamsParseResult SqlParamsParseResultModel=null;
				if(key.equals(andKey)){
					SqlParamsParseResultModel=processModelSqlWhereParams((Map<String,Object>)value,AND,isPlaceHolder);
				}else if(key.equals(orKey)){
					SqlParamsParseResultModel=processModelSqlWhereParams((Map<String,Object>)value,OR,isPlaceHolder);
				}
				if(SqlParamsParseResultModel!=null && StringUtils.isNotEmpty(SqlParamsParseResultModel.getBaseWhereSql())){
					sqlParamsParseResult.addSqlModel(andOrDelititer);
					sqlParamsParseResult.addSqlModel("("+SqlParamsParseResultModel.getBaseWhereSql()+")");
					sqlParamsParseResult.addArguments(SqlParamsParseResultModel.getArguments());
				}
			}else{
				//這裏纔是使用已經註冊的解析器進行解析
				SqlParamsParseItemResult sqlParamsParseItemResult=processNormalSqlWhereParams(key,value,isPlaceHolder);
				if(sqlParamsParseItemResult!=null){
					sqlParamsParseResult.addSqlModel(andOrDelititer);
					sqlParamsParseResult.addSqlModel(sqlParamsParseItemResult.getSqlModel(isPlaceHolder,PLACE_HOLDER));
					sqlParamsParseResult.addArgument(sqlParamsParseItemResult.getValue());
				}
			}
		}
		StringBuilder baseWhereSql=sqlParamsParseResult.getBaseWhereSql();
		if(StringUtils.isNotEmpty(baseWhereSql)){
			sqlParamsParseResult.setBaseWhereSql(new StringBuilder(baseWhereSql.substring(andOrDelititer.length())));
		}
	}
	return sqlParamsParseResult;
}

這裏進行了遞歸調用,主要用於處理 $and $or 的嵌套查詢,getSqlWhereParamsResultByAndOr可能內部調用了processModelSqlWhereParams,processModelSqlWhereParams內部又調用了getSqlWhereParamsResultByAndOr

private SqlParamsParseResult processModelSqlWhereParams(Map<String,Object> params,String andOr,boolean isPlaceHolder){
	return getSqlWhereParamsResultByAndOr(params,andOr,isPlaceHolder,new SqlParamsParseResult());
}

這裏就是使用解析器進行解析的過程,先遍歷每一個解析器是否支持當前的操做符,若是支持則進行相應的解析

private SqlParamsParseItemResult processNormalSqlWhereParams(String key,Object value,boolean isPlaceHolder) {
	SqlParamsParseItemResult sqlParamsParseItemResult=null;
	String[] parts=key.split(separatorFlag);
	if(parts.length==2){
		for(SqlParamsParser sqlParamsParser:sqlParamsParsers){
			if(sqlParamsParser.support(parts[1])){
				sqlParamsParseItemResult=sqlParamsParser.getParamsResult(parts[0],value,parts[1]);
				break;
			}
		}
	}else{
		sqlParamsParseItemResult=new SqlParamsParseItemResult(key,"=",SqlStringUtils.processString(value));
	}
	return sqlParamsParseItemResult;
}

##4.3 對外留出的擴展

{
	"c.age@>":14,
	"$or":{
		"b.id@=":2,
		"d.age@<":23
	}
}

這裏面的@ $or 以及 $and 都是能夠本身設定的,默認值是上述形式。

#5 工程項目

這個小項目已經發布到osc上,見 osc的search-sqlparams項目

相關文章
相關標籤/搜索