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