1. 程式人生 > >對Spring中MappingSqlQueryWithParameters、SqlQuery等的一些理解

對Spring中MappingSqlQueryWithParameters、SqlQuery等的一些理解

MappingSqlQueryWithParameters、SqlQuery等都是在Spring的org.springframework.jdbc.object包中。從包名object中也可以看出來這裡面放的是物件,主要是查詢物件。顧名思義,就是將查詢這個操作封裝成了一個物件,這裡麵包括查詢所使用的sql語句、引數、引數型別、查詢結果等。這樣這個查詢操作物件就是可以重複使用的,下次可以直接使用這個物件,不需要再重新構造sql語句,重新賦值引數,重新查詢,重新rowMapper等。

首先看一下類圖:

MappingSqlQueryWithParameters繼承關係類圖

從類圖中可以看出MappingSqlQueryWithParameters繼承字SqlQuery,SqlQuery繼承SqlOperation,SqlOperation繼承RdbmsOperation

MappingSqlQueryWithParameters類比較簡單,主要是實現了SqlQuery中的

protected RowMapper<T> newRowMapper(Object[] parameters, Map context)
虛擬函式。在該函式中建立了一個實現了RowMapper<T>的類RowMapperImpl的物件並返回。RowMapperImpl是在MappingSqlQueryWithParameters類中定義的內部類。
/**
 * Implementation of RowMapper that calls the enclosing
 * class's {@code mapRow} method for each row.
 */
protected class RowMapperImpl implements RowMapper<T> {

	private final Object[] params;

	private final Map context;

	/**
	 * Use an array results. More efficient if we know how many results to expect.
	 */
	public RowMapperImpl(Object[] parameters, Map context) {
		this.params = parameters;
		this.context = context;
	}
	//該類的mapRow方法實現就是呼叫MappingSqlQueryWithParameters類中的mapRow方法
	//因此繼承MappingSqlQueryWithParameters類的子類需要實現mapRow方法。
	public T mapRow(ResultSet rs, int rowNum) throws SQLException {
		return MappingSqlQueryWithParameters.this.mapRow(rs, rowNum, this.params, this.context);
	}
}

可以看出該類的mapRow方法實現就是呼叫MappingSqlQueryWithParameters類中的mapRow方法。因此繼承MappingSqlQueryWithParameters類的子類只需要實現mapRow方法就好,不需要再實現一個繼承RowMapper<T>介面的類,省下了一些程式碼量,這就是該類相比SqlQuery類的作用。

下面我們來看一下SqlQuery類。該類的作用主要是提供了很多形式的Excute函式和executeByNamedParam函式來分別執行sql語句,包括匿名引數的和命名引數的。

另外在此基礎上又提供了各種形式的findObject和findObjectByNamedParam函式來提供對單個物件的查詢操作。

這個類裡面最終呼叫的查詢函式有兩個,分別是:

/**
 * Central execution method. All un-named parameter execution goes through this method.
 * @param params parameters, similar to JDO query parameters.
 * Primitive parameters must be represented by their Object wrapper type.
 * The ordering of parameters is significant.
 * @param context contextual information passed to the {@code mapRow}
 * callback method. The JDBC operation itself doesn't rely on this parameter,
 * but it can be useful for creating the objects of the result list.
 * @return a List of objects, one per row of the ResultSet. Normally all these
 * will be of the same class, although it is possible to use different types.
 */
public List<T> execute(Object[] params, Map context) throws DataAccessException {
	//驗證傳進來的sql引數
	validateParameters(params);
	//呼叫newRowMapper函式生成RowMapper<T>具體類的物件
	RowMapper<T> rowMapper = newRowMapper(params, context);
	//呼叫相關的JdbcTemplate類的query函式來執行查詢
	return getJdbcTemplate().query(newPreparedStatementCreator(params), rowMapper);
}

/**
 * Central execution method. All named parameter execution goes through this method.
 * @param paramMap parameters associated with the name specified while declaring
 * the SqlParameters. Primitive parameters must be represented by their Object wrapper
 * type. The ordering of parameters is not significant since they are supplied in a
 * SqlParameterMap which is an implementation of the Map interface.
 * @param context contextual information passed to the {@code mapRow}
 * callback method. The JDBC operation itself doesn't rely on this parameter,
 * but it can be useful for creating the objects of the result list.
 * @return a List of objects, one per row of the ResultSet. Normally all these
 * will be of the same class, although it is possible to use different types.
 */
public List<T> executeByNamedParam(Map<String, ?> paramMap, Map context) throws DataAccessException {
	//驗證傳進來的sql引數
	validateNamedParameters(paramMap);
	//解析sql語句,找到每個佔位符和命名引數的位置,對於命名引數記錄其在sql語句中的名稱以及其起始和終止位置,
	//並記錄匿名引數以及命名引數的個數,以及佔位符的總個數。將所有這些引數解析出來後構造成ParsedSql物件進行儲存
	//但是合法的sql語句中不允許即出現命名引數佔位符和匿名引數佔位符。這個會在下面的buildValueArray函式中進行校驗
	ParsedSql parsedSql = getParsedSql();
	//將paramMap封裝為MapSqlParameterSource類物件
	MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap);
	//將sql語句中的命名引數佔位符替換為JDBC佔位符?形式。並且如果給的引數值是陣列或列表型別的話,就把命名引數展開成所需要數目的?佔位符
	//詳細處理情況可以參考substituteNamedParameters函式
	String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource);
	//將Map形式的引數轉換為陣列形式的引數
	Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters());
	//呼叫newRowMapper函式生成RowMapper<T>具體類的物件
	RowMapper<T> rowMapper = newRowMapper(params, context);
	//呼叫相關的JdbcTemplate類的query函式來執行查詢
	return getJdbcTemplate().query(newPreparedStatementCreator(sqlToUse, params), rowMapper);
}

只需要理解了這兩個函式,SqlQuery類就算是理解了。

在excuteByNameParam函式中呼叫的getParsedSql函式以及NamedParameterUtils的substituteNamedParameters函式需要深入瞭解下。設計和處理的都是很巧妙的。

getParsedSql函式本身比較簡單,主要呼叫了NamedParameterUtils的parseSqlStatement函式。那麼下面我們就來主要看看NamedParameterUtils的parseSqlStatement還有substituteNamedParameters函式吧。

parseSqlStatement函式如下:

/**
 * Parse the SQL statement and locate any placeholders or named parameters.
 * Named parameters are substituted for a JDBC placeholder.
 * @param sql the SQL statement
 * @return the parsed statement, represented as ParsedSql instance
 */
public static ParsedSql parseSqlStatement(final String sql) {
	Assert.notNull(sql, "SQL must not be null");

	Set<String> namedParameters = new HashSet<String>();
	String sqlToUse = sql;
	List<ParameterHolder> parameterList = new ArrayList<ParameterHolder>();

	char[] statement = sql.toCharArray();
	int namedParameterCount = 0;
	int unnamedParameterCount = 0;
	int totalParameterCount = 0;

	int escapes = 0;
	int i = 0;
	while (i < statement.length) {
		int skipToPosition = i;
		//將sql轉換成char資料,進行逐個字元的遍歷處理
		while (i < statement.length) {
			//從i位置開始跳過sql語句中的註釋和引號,返回跳過後sql語句下個待處理的位置
			//如果i開始的位置不是註釋或引號的開始,則會直接返回i,因為i就是待處理的位置
			//具體檢視skipCommentsAndQuotes是如何處理的
			skipToPosition = skipCommentsAndQuotes(statement, i);
			//表明當前i位置不需要跳過
			if (i == skipToPosition) {
				break;
			}
			//直接跳過註釋或者引號,到下一個待處理的位置
			else {
				i = skipToPosition;
			}
		}
		//表明處理到了sql語句的結尾處,已經處理完成
		if (i >= statement.length) {
			break;
		}
		char c = statement[i];
		if (c == ':' || c == '&') {
			//j指向i的下一個字元
			int j = i + 1;
			//如果是::,是Postgres的關鍵字,代表的是型別轉換操作符,後面不是引數,直接跳過
			if (j < statement.length && statement[j] == ':' && c == ':') {
				// Postgres-style "::" casting operator - to be skipped.
				i = i + 2;
				continue;
			}
			String parameter = null;
			if (j < statement.length && c == ':' && statement[j] == '{') {
				// :{x} style parameter
				while (j < statement.length && !('}' == statement[j])) {
					j++;
					if (':' == statement[j] || '{' == statement[j]) {
						throw new InvalidDataAccessApiUsageException("Parameter name contains invalid character '" +
								statement[j] + "' at position " + i + " in statement: " + sql);
					}
				}
				if (j >= statement.length) {
					throw new InvalidDataAccessApiUsageException(
							"Non-terminated named parameter declaration at position " + i + " in statement: " + sql);
				}
				if (j - i > 3) {
					parameter = sql.substring(i + 2, j);
					//將命名引數名稱加入namedParameters,並返回當前命名引數的個數
					namedParameterCount = addNewNamedParameter(namedParameters, namedParameterCount, parameter);
					//將命名引數封裝成ParameterHolder物件加入parameterList,包括引數的名稱,引數在sqlToUse中的起始和終止位置,
					//並返回當前引數的總個數。其中escapes表示遍歷到當前位置時statement的轉義字元的個數。
					//因為要在起始位置和終止位置中減掉其大小。因為在sqlToUse中會過濾掉轉義字元
					totalParameterCount = addNamedParameter(parameterList, totalParameterCount, escapes, i, j + 1, parameter);
				}
				j++;
			}
			else {
				//在命名引數內進行遍歷,直到命名引數分割符處
				while (j < statement.length && !isParameterSeparator(statement[j])) {
					j++;
				}
				if (j - i > 1) {
					parameter = sql.substring(i + 1, j);
					//將命名引數名稱加入namedParameters,並返回當前命名引數的個數
					namedParameterCount = addNewNamedParameter(namedParameters, namedParameterCount, parameter);
					//將命名引數封裝成ParameterHolder物件加入parameterList,並返回當前引數的總個數
					totalParameterCount = addNamedParameter(parameterList, totalParameterCount, escapes, i, j, parameter);
				}
			}
			//i跳到分割符處,因為後面還要i++,所以這裡要=j-1
			i = j - 1;
		}
		else {
			//判斷是否轉義字元
			if (c == '\\') {
				int j = i + 1;
				if (j < statement.length && statement[j] == ':') {
					// this is an escaped : and should be skipped
					sqlToUse = sqlToUse.substring(0, i - escapes) + sqlToUse.substring(i - escapes + 1);
					escapes++;
					i = i + 2;
					continue;
				}
			}
			if (c == '?') {
				unnamedParameterCount++;
				totalParameterCount++;
			}
		}
		i++;
	}
	//構造ParsedSql物件
	ParsedSql parsedSql = new ParsedSql(sqlToUse);
	for (ParameterHolder ph : parameterList) {
		parsedSql.addNamedParameter(ph.getParameterName(), ph.getStartIndex(), ph.getEndIndex());
	}
	parsedSql.setNamedParameterCount(namedParameterCount);
	parsedSql.setUnnamedParameterCount(unnamedParameterCount);
	parsedSql.setTotalParameterCount(totalParameterCount);
	return parsedSql;
}
這裡面用到的skipCommentsAndQuotes函式是用來跳過當前位置開始的註釋或者是引號符,然後返回註釋或者引號結束的位置。
/**
 * Skip over comments and quoted names present in an SQL statement
 * @param statement character array containing SQL statement
 * @param position current position of statement
 * @return next position to process after any comments or quotes are skipped
 */
private static int skipCommentsAndQuotes(char[] statement, int position) {
	for (int i = 0; i < START_SKIP.length; i++) {
		//判斷statement的position位置是不是和START_SKIP定義的一些字元相等,如果不相等,下面直接返回
		if (statement[position] == START_SKIP[i].charAt(0)) {
			boolean match = true;
			for (int j = 1; j < START_SKIP[i].length(); j++) {
				if (!(statement[position + j] == START_SKIP[i].charAt(j))) {
					match = false;
					break;
				}
			}
			if (match) {
				int offset = START_SKIP[i].length();
				for (int m = position + offset; m < statement.length; m++) {
					//註釋符或引號要成對出現,所以直接跟STOP_SKIP[i]進行比較
					if (statement[m] == STOP_SKIP[i].charAt(0)) {
						boolean endMatch = true;
						int endPos = m;
						for (int n = 1; n < STOP_SKIP[i].length(); n++) {
							if (m + n >= statement.length) {
								// last comment not closed properly
								return statement.length;
							}
							if (!(statement[m + n] == STOP_SKIP[i].charAt(n))) {
								endMatch = false;
								break;
							}
							endPos = m + n;
						}
						if (endMatch) {
							// found character sequence ending comment or quote
							return endPos + 1;
						}
					}
				}
				// character sequence ending comment or quote not found
				return statement.length;
			}

		}
	}
	return position;
}
另外一個substituteNamedParameters函式如下:
/**
 * Parse the SQL statement and locate any placeholders or named parameters. Named
 * parameters are substituted for a JDBC placeholder, and any select list is expanded
 * to the required number of placeholders. Select lists may contain an array of
 * objects, and in that case the placeholders will be grouped and enclosed with
 * parentheses. This allows for the use of "expression lists" in the SQL statement
 * like: <br /><br />
 * {@code select id, name, state from table where (name, age) in (('John', 35), ('Ann', 50))}
 * <p>The parameter values passed in are used to determine the number of placeholders to
 * be used for a select list. Select lists should be limited to 100 or fewer elements.
 * A larger number of elements is not guaranteed to be supported by the database and
 * is strictly vendor-dependent.
 * @param parsedSql the parsed representation of the SQL statement
 * @param paramSource the source for named parameters
 * @return the SQL statement with substituted parameters
 * @see #parseSqlStatement
 */
public static String substituteNamedParameters(ParsedSql parsedSql, SqlParameterSource paramSource) {
	String originalSql = parsedSql.getOriginalSql();
	StringBuilder actualSql = new StringBuilder();
	List paramNames = parsedSql.getParameterNames();
	int lastIndex = 0;
	for (int i = 0; i < paramNames.size(); i++) {
		String paramName = (String) paramNames.get(i);
		int[] indexes = parsedSql.getParameterIndexes(i);
		int startIndex = indexes[0];
		int endIndex = indexes[1];
		//將originalSql上一個位置到本次命名引數位置之間的字串加入到actualSql中。
		//因為命名引數在下面要替換或者展開為佔位符?
		actualSql.append(originalSql, lastIndex, startIndex);
		if (paramSource != null && paramSource.hasValue(paramName)) {
			Object value = paramSource.getValue(paramName);
			//獲取引數具體的值,因為傳過來的命名引數的值有可能是SqlParameterValue物件
			if (value instanceof SqlParameterValue) {
				value = ((SqlParameterValue) value).getValue();
			}
			//如果物件是集合,就需要將sql語句的命名引數進行替換為和集合中值個數對應的佔位符?
			if (value instanceof Collection) {
				Iterator entryIter = ((Collection) value).iterator();
				int k = 0;
				while (entryIter.hasNext()) {
					if (k > 0) {
						actualSql.append(", ");
					}
					k++;
					Object entryItem = entryIter.next();
					//如果集合中的每個值又是個物件陣列,說明最終是諸如“(name, age) in (('John', 35), ('Ann', 50))”
					//這樣的情形,需要在sql語句中將每個集合中的陣列元素對應成(?,?,...)這樣的形式,每個元組中佔位符的個數和Object[]陣列的大小相等
					//最終整個該命名引數會替換為((?,?,...),(?,?,...),...)的形式,元組的個數和集合大小相等
					if (entryItem instanceof Object[]) {
						Object[] expressionList = (Object[]) entryItem;
						actualSql.append("(");
						for (int m = 0; m < expressionList.length; m++) {
							if (m > 0) {
								actualSql.append(", ");
							}
							actualSql.append("?");
						}
						actualSql.append(")");
					}
					//集合中的每個物件就是單個的物件,那麼每個集合中的物件元素就替換為?,
					//最終整個該命名引數會替換為(?,?,...)的形式,佔位符的個數和集合大小相等
					else {
						actualSql.append("?");
					}
				}
			}
			//引數值不是集合,直接將命名引數替換為?
			else {
				actualSql.append("?");
			}
		}
		//如果paramSource不包含當前的命名引數,直接將命名引數替換為?
		else {
			actualSql.append("?");
		}
		lastIndex = endIndex;
	}
	actualSql.append(originalSql, lastIndex, originalSql.length());
	return actualSql.toString();
}

理解了這些,SqlQuery類也就能夠理解了。

SqlQuery又繼承自RdbmsOperation類。這個類主要定義了Query Object的一些基本屬性和操作方法。

其中validateParameters、validateNamedParameters函式分別被SqlQuery中的execute和executeByNamedParam函式呼叫,用來驗證引數。

在validateParameters、validateNamedParameters這兩個函式中,都用到了declaredParameters屬性。該屬性表示了sql引數的定義,包括引數的名稱、型別等。

因此在建立SqlQuery相關物件的時候,應該先賦值declaredParameters屬性。然後才能使用excute或者executeByNamedParam等函式。可以呼叫setTypes、declareParameter、setParameters等函式來賦值declaredParameters屬性。