1. 程式人生 > >深入瞭解MyBatis返回值

深入瞭解MyBatis返回值

想了解返回值,我們需要了解resultType,resultMap以及介面方法中定義的返回值。

我們先看resultType和resultMap
resultType和resultMap

大家應該都知道在MyBatis的<select>標籤中有兩種設定返回值的方式,分別是resultMap和resultType。

處理resultMap和resultType的程式碼如下:

private void setStatementResultMap(
        String resultMap,
        Class<?> resultType,
        ResultSetType resultSetType,
        MappedStatement.Builder statementBuilder) {
    resultMap = applyCurrentNamespace(resultMap, true);

    List<ResultMap> resultMaps = new ArrayList<ResultMap>();
    if (resultMap != null) {
        String[] resultMapNames = resultMap.split(",");
        for (String resultMapName : resultMapNames) {
            try {
                resultMaps.add(configuration.getResultMap(resultMapName.trim()));
            } catch (IllegalArgumentException e) {
                throw new IncompleteElementException("Could not find result map " + resultMapName, e);
            }
        }
    } else if (resultType != null) {
        ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(
                configuration,
                statementBuilder.id() + "-Inline",
                resultType,
                new ArrayList<ResultMapping>(),
                null);
        resultMaps.add(inlineResultMapBuilder.build());
    }
    statementBuilder.resultMaps(resultMaps);

    statementBuilder.resultSetType(resultSetType);
}


可以看到這裡會優先處理resultMap,但是也使用了resultType。

接下來看MyBatis獲取資料後,如果處理一行結果(以簡單資料為例,不考慮巢狀情況):

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    Object resultObject = createResultObject(rsw, resultMap, lazyLoader, null);
    if (resultObject != null && !typeHandlerRegistry.hasTypeHandler(resultMap.getType())) {
        final MetaObject metaObject = configuration.newMetaObject(resultObject);
        boolean foundValues = resultMap.getConstructorResultMappings().size() > 0;
        if (shouldApplyAutomaticMappings(resultMap, !AutoMappingBehavior.NONE.equals(configuration.getAutoMappingBehavior()))) {
            foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
        }
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
        foundValues = lazyLoader.size() > 0 || foundValues;
        resultObject = foundValues ? resultObject : null;
        return resultObject;
    }
    return resultObject;
}

上面這段程式碼中重要的程式碼如下:

if (shouldApplyAutomaticMappings(resultMap, !AutoMappingBehavior.NONE.equals(configuration.getAutoMappingBehavior()))) {
    foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
}
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;

if中判斷的是當前是否支援自動對映(可以配置),這一點很重要,如果不支援,那麼沒法使用resultType方式,必須用resultMap方式,如果支援,resultType方式和resultMap方式可以同時使用。

這裡的基本邏輯是先對沒有resultMap的屬性自動對映賦值,通過applyAutomaticMappings實現。

如果物件有resultMap,那麼還會進行applyPropertyMappings方法。

也就是先處理resultType中自動對映的欄位,在處理resultMap中的配置的欄位,兩者可以同時使用!

下面按照順序分別說兩種方式。
resultType方式

如果支援自動對映,那麼會執行applyAutomaticMappings,這裡面有metaObject引數。

final MetaObject metaObject = configuration.newMetaObject(resultObject);


我們看看建立metaObject最關鍵的一個地方,在Reflector類中:

for (String propName : readablePropertyNames) {
    caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}

這裡將實體中的屬性名,做了一個對映,是大寫的對應實際的屬性名。例如ID:id。

在applyAutomaticMappings中的第一行,首先獲取沒有對映的列名:

final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);


獲取列名的時候:

for (String columnName : columnNames) {
    final String upperColumnName = columnName.toUpperCase(Locale.ENGLISH);
    if (mappedColumns.contains(upperColumnName)) {
        mappedColumnNames.add(upperColumnName);
    } else {
        unmappedColumnNames.add(columnName);
    }
}

注意這裡將列名轉換為大寫形式,同時儲存了mappedColumnNames對映的列和unmappedColumnNames未對映的列。

因為不管是屬性名還是查詢列都是大寫的,所以只要列名和屬性名大寫一致,就會匹配上。

因此我們在寫sql的時候,不需要對查詢列的大小寫進行轉換,自動匹配是不區分大小寫的。
resultMap方式

這種方式也很簡單,上面提到了mappedColumnNames,在判斷是否為對映列的時候,使用mappedColumns.contains(upperColumnName)進行判斷,mappedColumns是我們配置的對映的列,那是不是我們配置的時候必須大寫呢?

實際上不用,這裡也不區分大小寫,在<result column="xxx" ../>的column也不區分大小寫,看下面的程式碼:

for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {
    final String compositeColumn = compositeResultMapping.getColumn();
    if (compositeColumn != null) {
        resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));
    }
}

這裡也轉換為了大寫。

到這裡關於resultTypt和resultMap就結束了,但是有一個簡單的問題,很多人不懂,是什麼?看下個標題。
MyBatis介面返回值

介面返回值通常是一個結果,或者是List和陣列。
MyBatis如何知道我想要返回一個結果還是多個結果?

在MapperMethod中的部分程式碼如下:

if (method.returnsVoid() && method.hasResultHandler()) {
    executeWithResultHandler(sqlSession, args);
    result = null;
} else if (method.returnsMany()) {
    result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
    result = executeForMap(sqlSession, args);
} else {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = sqlSession.selectOne(command.getName(), param);
}


可以看到查詢結果有4中情況,void,list(和array),map,one。

這裡重要就是if的判斷條件,這種判斷條件計算方法:

this.returnType = method.getReturnType();
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray());

可以看到,這些條件完全就是通過方法的返回值決定的。所以如果你寫的返回值是陣列或者集合,返回的結果就是多個。

如果返回值本身有多個,但是返回值寫了一個POJO,不是集合或者陣列時會怎樣?

答案是會報錯TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size())。

不管是返回一個結果還是多個結果,MyBatis都是安裝多個結果進行查詢,selectOne是查詢一個,selectList是查詢多個,我們看看selectOne程式碼:

public <T> T selectOne(String statement, Object parameter) {
    List<T> list = this.<T>selectList(statement, parameter);
    if (list.size() == 1) {
        return list.get(0);
    } else if (list.size() > 1) {
        throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
        return null;
    }
}

注意看:

List<T> list = this.<T>selectList(statement, parameter);

實際上,不管查詢一個還是多個結果,MyBatis都是先按多個結果進行查詢。拿到list結果後在判斷。

如果是查詢一個結果的情況,那麼list最多隻能有一個返回值。通過上面程式碼的if else if esle可以很明白的理解。
resultTyp,resultMap和返回值多少有關係嗎?

沒有任何關係。

通過前面resultType和resultMap的內容,我們應該知道,這個屬性是配置JDBC查詢結果如何對映到一個物件上的。

不管返回值是什麼或者是幾個,都是按照resultType和resultMap生成返回結果。

返回結果的型別由resultType和resultMap決定。
返回結果的型別

返回結果的型別由resultType和resultMap決定,是不是很詫異???

實際上就是這種情況。。

舉個例子,有個實體Country和Country2。

介面中List<Country> selectAll(),xml中的<select id="selectAll" resultType="Country2">.

當你通過介面呼叫的時候,返回值是什麼?你以為自己的List中的物件型別是Country,但他們實際上都是Country2

如果介面方法為Country selectById(Integer id),xml中為<select id="selectById" resultType="Country2">,由於型別不一致,查詢的時候才會報錯:java.lang.ClassCastException: xx.Country2 cannot be cast to xx.Country
為什麼會這樣呢?

這是因為介面呼叫方式是對名稱空間方式呼叫的封裝。

當你通過名稱空間方式呼叫的時候,返回結果的型別是什麼?

就是由resultType和resultMap決定的型別,這很容易理解。但是換成介面就覺得不一樣了。

這是由於介面方法方式多了返回值,所以我們會認為返回的一定是這個型別。實際上是錯的。
特殊情況

當使用純註解方式時,介面的返回值型別可以起到作用,如果沒有使用@ResultType註解指定返回值型別,那麼就會使用這裡寫的返回值型別作為resultType。