diff --git a/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/CreateDB.sql b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/CreateDB.sql new file mode 100644 index 000000000..becb52bf0 --- /dev/null +++ b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/CreateDB.sql @@ -0,0 +1,21 @@ +drop table user949 if exists; +drop table user_detail949 if exists; + +create table user949 +( + id integer NOT NULL PRIMARY KEY, + name varchar(32) +); + +create table user_detail949 +( + id integer NOT NULL PRIMARY KEY, + user_id integer, + email varchar(64) +); + +INSERT INTO user949 (id, name) VALUES (1, 'user1'); +INSERT INTO user949 (id, name) VALUES (2, 'user2'); + +INSERT INTO user_detail949 (id, user_id, email) VALUES (1, 1, 'user1@example.com'); +INSERT INTO user_detail949 (id, user_id, email) VALUES (2, 2, 'user2@example.com'); diff --git a/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/NestedQueryTest.java b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/NestedQueryTest.java new file mode 100644 index 000000000..d5a07d2b0 --- /dev/null +++ b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/NestedQueryTest.java @@ -0,0 +1,144 @@ +package tk.mybatis.mapper.issues._949_nested_query; + +import org.apache.ibatis.session.SqlSession; +import org.junit.Assert; +import org.junit.Test; +import tk.mybatis.mapper.base.BaseTest; +import tk.mybatis.mapper.entity.Config; + +import java.io.IOException; +import java.io.Reader; +import java.net.URL; +import java.util.List; + +/** + * Test for issue #949: nested queries fail when enableBaseResultMapFlag is true. + * + * The root cause: setRawSqlSourceMapper was replacing ANY user-defined resultMap whose + * resultMappings list was empty (e.g. a resultMap with only a discriminator, or only + * nested-select associations whose column is shared). The fix is to only replace + * auto-generated inline resultMaps (ID ends with "-Inline", created from resultType=), + * never user-configured resultMaps (created from resultMap=). + */ +public class NestedQueryTest extends BaseTest { + + @Override + protected Config getConfig() { + Config config = super.getConfig(); + config.setEnableBaseResultMapFlag(true); + return config; + } + + @Override + protected Reader getConfigFileAsReader() throws IOException { + URL url = NestedQueryTest.class.getResource("mybatis-config-issue949.xml"); + return toReader(url); + } + + @Override + protected Reader getSqlFileAsReader() throws IOException { + URL url = NestedQueryTest.class.getResource("CreateDB.sql"); + return toReader(url); + } + + /** + * A resultMap with explicit simple column mappings AND an association with nested select. + * resultMappings is NOT empty, so setRawSqlSourceMapper must not replace it. + * Verifies that the nested select (detail) is still executed. + */ + @Test + public void testNestedQueryWithExplicitMappings() { + SqlSession sqlSession = getSqlSession(); + try { + UserMapper mapper = sqlSession.getMapper(UserMapper.class); + List users = mapper.selectUsersWithDetail(); + + Assert.assertNotNull("Users should not be null", users); + Assert.assertEquals("Should have 2 users", 2, users.size()); + + for (User user : users) { + Assert.assertNotNull("User id should not be null", user.getId()); + Assert.assertNotNull("User name should not be null", user.getName()); + Assert.assertNotNull("User detail should not be null - nested select must have been executed", + user.getDetail()); + Assert.assertNotNull("UserDetail email should not be null", + user.getDetail().getEmail()); + } + } finally { + sqlSession.close(); + } + } + + /** + * A resultMap with ONLY an association (no explicit simple column mappings). + * resultMappings is NOT empty (contains the association), so setRawSqlSourceMapper + * must not replace it. The nested select (detail) must still be executed. + * Note: id is null because MyBatis auto-mapping skips "id" once it's consumed as + * the association column — that is expected behaviour for this resultMap configuration. + */ + @Test + public void testNestedQueryWithOnlyAssociation() { + SqlSession sqlSession = getSqlSession(); + try { + UserMapper mapper = sqlSession.getMapper(UserMapper.class); + List users = mapper.selectUsersWithDetailOnlyAssociation(); + + Assert.assertNotNull("Users should not be null", users); + Assert.assertEquals("Should have 2 users", 2, users.size()); + + for (User user : users) { + // The nested select (detail) should still be executed. + Assert.assertNotNull("User detail should not be null - nested select must have been executed", + user.getDetail()); + Assert.assertNotNull("UserDetail email should not be null", + user.getDetail().getEmail()); + } + } finally { + sqlSession.close(); + } + } + + /** + * Core bug scenario for issue #949. + * + * A user-defined resultMap with ONLY a discriminator has empty resultMappings. + * Before the fix, setRawSqlSourceMapper replaced it with the JPA-generated + * BaseMapperResultMap, wiping out the discriminator and breaking nested queries + * that lived inside the discriminator-case result maps. + * + * After the fix (only replace inline "-Inline" resultMaps), the discriminator + * result map is preserved: + * - user id=1 is routed to discriminatorCase1Map → nested select loads detail + * - user id=2 is routed to discriminatorCase2Map → no detail + */ + @Test + public void testNestedQueryWithDiscriminator() { + SqlSession sqlSession = getSqlSession(); + try { + UserMapper mapper = sqlSession.getMapper(UserMapper.class); + List users = mapper.selectUsersWithDiscriminator(); + + Assert.assertNotNull("Users should not be null", users); + Assert.assertEquals("Should have 2 users", 2, users.size()); + + // Find user1 and user2 + User user1 = users.stream().filter(u -> u.getId() != null && u.getId() == 1).findFirst().orElse(null); + User user2 = users.stream().filter(u -> u.getId() != null && u.getId() == 2).findFirst().orElse(null); + + Assert.assertNotNull("user1 should be found", user1); + Assert.assertNotNull("user2 should be found", user2); + + // user1 goes through discriminatorCase1Map which has a nested select for detail + Assert.assertNotNull( + "user1 detail should be loaded by nested select via discriminator case 1 - " + + "this fails without the fix because the discriminator resultMap was replaced", + user1.getDetail()); + Assert.assertEquals("user1@example.com", user1.getDetail().getEmail()); + + // user2 goes through discriminatorCase2Map which has no nested select + Assert.assertNull("user2 detail should be null (no nested select in case 2)", user2.getDetail()); + } finally { + sqlSession.close(); + } + } +} diff --git a/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/User.java b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/User.java new file mode 100644 index 000000000..713cce095 --- /dev/null +++ b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/User.java @@ -0,0 +1,47 @@ +package tk.mybatis.mapper.issues._949_nested_query; + +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * Entity for issue 949 - nested query test + */ +@Table(name = "user949") +public class User { + + @Id + private Integer id; + + private String name; + + private UserDetail detail; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public UserDetail getDetail() { + return detail; + } + + public void setDetail(UserDetail detail) { + this.detail = detail; + } + + @Override + public String toString() { + return "User{id=" + id + ", name='" + name + "', detail=" + detail + "}"; + } +} diff --git a/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/UserDetail.java b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/UserDetail.java new file mode 100644 index 000000000..73654f650 --- /dev/null +++ b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/UserDetail.java @@ -0,0 +1,49 @@ +package tk.mybatis.mapper.issues._949_nested_query; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * Entity for issue 949 - nested query test + */ +@Table(name = "user_detail949") +public class UserDetail { + + @Id + private Integer id; + + @Column(name = "user_id") + private Integer userId; + + private String email; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + @Override + public String toString() { + return "UserDetail{id=" + id + ", userId=" + userId + ", email='" + email + "'}"; + } +} diff --git a/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/UserMapper.java b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/UserMapper.java new file mode 100644 index 000000000..3803abf1c --- /dev/null +++ b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/UserMapper.java @@ -0,0 +1,37 @@ +package tk.mybatis.mapper.issues._949_nested_query; + +import tk.mybatis.mapper.common.BaseMapper; + +import java.util.List; + +/** + * Mapper for issue 949 - nested query test + */ +public interface UserMapper extends BaseMapper { + + /** + * Query using a resultMap that has an association with a nested select. + * With enableBaseResultMapFlag=true, this resultMap should NOT be replaced + * even though the entity class (User) is a JPA entity. + */ + List selectUsersWithDetail(); + + /** + * Query using a resultMap that has ONLY an association (no explicit simple mappings). + * This tests the edge case where resultMappings may be non-empty but only with association. + */ + List selectUsersWithDetailOnlyAssociation(); + + /** + * Query using a resultMap that has ONLY a discriminator (empty resultMappings). + * Without the fix, setRawSqlSourceMapper would replace this resultMap with the JPA-generated + * BaseMapperResultMap, losing the discriminator and breaking nested queries in case maps. + * With the fix, this user-defined resultMap is preserved as-is. + */ + List selectUsersWithDiscriminator(); + + /** + * Nested select for UserDetail by userId. + */ + UserDetail selectDetailByUserId(Integer userId); +} diff --git a/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/UserMapper.xml b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/UserMapper.xml new file mode 100644 index 000000000..678fda70d --- /dev/null +++ b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/UserMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/mybatis-config-issue949.xml b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/mybatis-config-issue949.xml new file mode 100644 index 000000000..995fcda47 --- /dev/null +++ b/base/src/test/java/tk/mybatis/mapper/issues/_949_nested_query/mybatis-config-issue949.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/java/tk/mybatis/mapper/mapperhelper/MapperHelper.java b/core/src/main/java/tk/mybatis/mapper/mapperhelper/MapperHelper.java index 913d4a074..ba8e1f45e 100644 --- a/core/src/main/java/tk/mybatis/mapper/mapperhelper/MapperHelper.java +++ b/core/src/main/java/tk/mybatis/mapper/mapperhelper/MapperHelper.java @@ -406,8 +406,14 @@ public void setSqlSource(MappedStatement ms, MapperTemplate mapperTemplate) { */ public void setRawSqlSourceMapper(MappedStatement ms) { ResultMap rm = ms.getResultMaps().get(0); - //不处理已经配置映射的查询 - if (rm.getResultMappings().isEmpty()) { + // Only replace auto-generated inline result maps (from XML resultType= or annotation @Select + // without @Results). These have IDs derived from the statement ID with a hyphen suffix, e.g.: + // XML resultType= → statementId + "-Inline" + // Annotation without @Results → statementId + "-void" or statementId + "-TypeName" + // User-defined result maps (XML or @ResultMap) have IDs independent of + // the statement ID, so they won't match and will be left intact — preserving nested queries, + // discriminators, and other custom configurations. + if (rm.getResultMappings().isEmpty() && rm.getId().startsWith(ms.getId() + "-")) { EntityTable entityTable = EntityHelper.getEntityTableOrNull(rm.getType()); if (entityTable != null) { List resultMaps = new ArrayList<>(); diff --git a/org/apache/ibatis/builder/MapperBuilderAssistant.java b/org/apache/ibatis/builder/MapperBuilderAssistant.java new file mode 100644 index 000000000..568d7c6aa --- /dev/null +++ b/org/apache/ibatis/builder/MapperBuilderAssistant.java @@ -0,0 +1,470 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.builder; + +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; + +import org.apache.ibatis.cache.Cache; +import org.apache.ibatis.cache.decorators.LruCache; +import org.apache.ibatis.cache.impl.PerpetualCache; +import org.apache.ibatis.executor.ErrorContext; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.mapping.CacheBuilder; +import org.apache.ibatis.mapping.Discriminator; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.ParameterMap; +import org.apache.ibatis.mapping.ParameterMapping; +import org.apache.ibatis.mapping.ParameterMode; +import org.apache.ibatis.mapping.ResultFlag; +import org.apache.ibatis.mapping.ResultMap; +import org.apache.ibatis.mapping.ResultMapping; +import org.apache.ibatis.mapping.ResultSetType; +import org.apache.ibatis.mapping.SqlCommandType; +import org.apache.ibatis.mapping.SqlSource; +import org.apache.ibatis.mapping.StatementType; +import org.apache.ibatis.reflection.MetaClass; +import org.apache.ibatis.scripting.LanguageDriver; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.TypeHandler; + +/** + * @author Clinton Begin + */ +public class MapperBuilderAssistant extends BaseBuilder { + + private String currentNamespace; + private final String resource; + private Cache currentCache; + private boolean unresolvedCacheRef; // issue #676 + + public MapperBuilderAssistant(Configuration configuration, String resource) { + super(configuration); + ErrorContext.instance().resource(resource); + this.resource = resource; + } + + public String getCurrentNamespace() { + return currentNamespace; + } + + public void setCurrentNamespace(String currentNamespace) { + if (currentNamespace == null) { + throw new BuilderException("The mapper element requires a namespace attribute to be specified."); + } + + if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) { + throw new BuilderException( + "Wrong namespace. Expected '" + this.currentNamespace + "' but found '" + currentNamespace + "'."); + } + + this.currentNamespace = currentNamespace; + } + + public String applyCurrentNamespace(String base, boolean isReference) { + if (base == null) { + return null; + } + if (isReference) { + // is it qualified with any namespace yet? + if (base.contains(".")) { + return base; + } + } else { + // is it qualified with this namespace yet? + if (base.startsWith(currentNamespace + ".")) { + return base; + } + if (base.contains(".")) { + throw new BuilderException("Dots are not allowed in element names, please remove it from " + base); + } + } + return currentNamespace + "." + base; + } + + public Cache useCacheRef(String namespace) { + if (namespace == null) { + throw new BuilderException("cache-ref element requires a namespace attribute."); + } + try { + unresolvedCacheRef = true; + Cache cache = configuration.getCache(namespace); + if (cache == null) { + throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found."); + } + currentCache = cache; + unresolvedCacheRef = false; + return cache; + } catch (IllegalArgumentException e) { + throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e); + } + } + + public Cache useNewCache(Class typeClass, Class evictionClass, Long flushInterval, + Integer size, boolean readWrite, boolean blocking, Properties props) { + Cache cache = new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class)) + .addDecorator(valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size) + .readWrite(readWrite).blocking(blocking).properties(props).build(); + configuration.addCache(cache); + currentCache = cache; + return cache; + } + + public ParameterMap addParameterMap(String id, Class parameterClass, List parameterMappings) { + id = applyCurrentNamespace(id, false); + ParameterMap parameterMap = new ParameterMap.Builder(configuration, id, parameterClass, parameterMappings).build(); + configuration.addParameterMap(parameterMap); + return parameterMap; + } + + public ParameterMapping buildParameterMapping(Class parameterType, String property, Class javaType, + JdbcType jdbcType, String resultMap, ParameterMode parameterMode, Class> typeHandler, + Integer numericScale) { + resultMap = applyCurrentNamespace(resultMap, true); + + // Class parameterType = parameterMapBuilder.type(); + Class javaTypeClass = resolveParameterJavaType(parameterType, property, javaType, jdbcType); + TypeHandler typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler); + + return new ParameterMapping.Builder(configuration, property, javaTypeClass).jdbcType(jdbcType) + .resultMapId(resultMap).mode(parameterMode).numericScale(numericScale).typeHandler(typeHandlerInstance).build(); + } + + public ResultMap addResultMap(String id, Class type, String extend, Discriminator discriminator, + List resultMappings, Boolean autoMapping) { + id = applyCurrentNamespace(id, false); + extend = applyCurrentNamespace(extend, true); + + if (extend != null) { + if (!configuration.hasResultMap(extend)) { + throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'"); + } + ResultMap resultMap = configuration.getResultMap(extend); + List extendedResultMappings = new ArrayList<>(resultMap.getResultMappings()); + extendedResultMappings.removeAll(resultMappings); + // Remove parent constructor if this resultMap declares a constructor. + boolean declaresConstructor = false; + for (ResultMapping resultMapping : resultMappings) { + if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) { + declaresConstructor = true; + break; + } + } + if (declaresConstructor) { + extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)); + } + resultMappings.addAll(extendedResultMappings); + } + ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping) + .discriminator(discriminator).build(); + configuration.addResultMap(resultMap); + return resultMap; + } + + public Discriminator buildDiscriminator(Class resultType, String column, Class javaType, JdbcType jdbcType, + Class> typeHandler, Map discriminatorMap) { + ResultMapping resultMapping = buildResultMapping(resultType, null, column, javaType, jdbcType, null, null, null, + null, typeHandler, new ArrayList<>(), null, null, false); + Map namespaceDiscriminatorMap = new HashMap<>(); + for (Map.Entry e : discriminatorMap.entrySet()) { + String resultMap = e.getValue(); + resultMap = applyCurrentNamespace(resultMap, true); + namespaceDiscriminatorMap.put(e.getKey(), resultMap); + } + return new Discriminator.Builder(configuration, resultMapping, namespaceDiscriminatorMap).build(); + } + + public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, + SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class parameterType, + String resultMap, Class resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, + boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, + LanguageDriver lang, String resultSets, boolean dirtySelect) { + + if (unresolvedCacheRef) { + throw new IncompleteElementException("Cache-ref not yet resolved"); + } + + id = applyCurrentNamespace(id, false); + + MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType) + .resource(resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType) + .keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId).lang(lang) + .resultOrdered(resultOrdered).resultSets(resultSets) + .resultMaps(getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType) + .flushCacheRequired(flushCache).useCache(useCache).cache(currentCache).dirtySelect(dirtySelect); + + ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id); + if (statementParameterMap != null) { + statementBuilder.parameterMap(statementParameterMap); + } + + MappedStatement statement = statementBuilder.build(); + configuration.addMappedStatement(statement); + return statement; + } + + /** + * Backward compatibility signature 'addMappedStatement'. + * + * @param id + * the id + * @param sqlSource + * the sql source + * @param statementType + * the statement type + * @param sqlCommandType + * the sql command type + * @param fetchSize + * the fetch size + * @param timeout + * the timeout + * @param parameterMap + * the parameter map + * @param parameterType + * the parameter type + * @param resultMap + * the result map + * @param resultType + * the result type + * @param resultSetType + * the result set type + * @param flushCache + * the flush cache + * @param useCache + * the use cache + * @param resultOrdered + * the result ordered + * @param keyGenerator + * the key generator + * @param keyProperty + * the key property + * @param keyColumn + * the key column + * @param databaseId + * the database id + * @param lang + * the lang + * + * @return the mapped statement + */ + public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, + SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class parameterType, + String resultMap, Class resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, + boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, + LanguageDriver lang, String resultSets) { + return addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, + parameterType, resultMap, resultType, resultSetType, flushCache, useCache, resultOrdered, keyGenerator, + keyProperty, keyColumn, databaseId, lang, null, false); + } + + public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, + SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class parameterType, + String resultMap, Class resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, + boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, + LanguageDriver lang) { + return addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, + parameterType, resultMap, resultType, resultSetType, flushCache, useCache, resultOrdered, keyGenerator, + keyProperty, keyColumn, databaseId, lang, null); + } + + private T valueOrDefault(T value, T defaultValue) { + return value == null ? defaultValue : value; + } + + private ParameterMap getStatementParameterMap(String parameterMapName, Class parameterTypeClass, + String statementId) { + parameterMapName = applyCurrentNamespace(parameterMapName, true); + ParameterMap parameterMap = null; + if (parameterMapName != null) { + try { + parameterMap = configuration.getParameterMap(parameterMapName); + } catch (IllegalArgumentException e) { + throw new IncompleteElementException("Could not find parameter map " + parameterMapName, e); + } + } else if (parameterTypeClass != null) { + List parameterMappings = new ArrayList<>(); + parameterMap = new ParameterMap.Builder(configuration, statementId + "-Inline", parameterTypeClass, + parameterMappings).build(); + } + return parameterMap; + } + + private List getStatementResultMaps(String resultMap, Class resultType, String statementId) { + resultMap = applyCurrentNamespace(resultMap, true); + + List resultMaps = new ArrayList<>(); + 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 + "' referenced from '" + statementId + "'", e); + } + } + } else if (resultType != null) { + ResultMap inlineResultMap = new ResultMap.Builder(configuration, statementId + "-Inline", resultType, + new ArrayList<>(), null).build(); + resultMaps.add(inlineResultMap); + } + return resultMaps; + } + + public ResultMapping buildResultMapping(Class resultType, String property, String column, Class javaType, + JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix, + Class> typeHandler, List flags, String resultSet, String foreignColumn, + boolean lazy) { + Class javaTypeClass = resolveResultJavaType(resultType, property, javaType); + TypeHandler typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler); + List composites; + if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) { + composites = Collections.emptyList(); + } else { + composites = parseCompositeColumnName(column); + } + return new ResultMapping.Builder(configuration, property, column, javaTypeClass).jdbcType(jdbcType) + .nestedQueryId(applyCurrentNamespace(nestedSelect, true)) + .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true)).resultSet(resultSet) + .typeHandler(typeHandlerInstance).flags(flags == null ? new ArrayList<>() : flags).composites(composites) + .notNullColumns(parseMultipleColumnNames(notNullColumn)).columnPrefix(columnPrefix).foreignColumn(foreignColumn) + .lazy(lazy).build(); + } + + /** + * Backward compatibility signature 'buildResultMapping'. + * + * @param resultType + * the result type + * @param property + * the property + * @param column + * the column + * @param javaType + * the java type + * @param jdbcType + * the jdbc type + * @param nestedSelect + * the nested select + * @param nestedResultMap + * the nested result map + * @param notNullColumn + * the not null column + * @param columnPrefix + * the column prefix + * @param typeHandler + * the type handler + * @param flags + * the flags + * + * @return the result mapping + */ + public ResultMapping buildResultMapping(Class resultType, String property, String column, Class javaType, + JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix, + Class> typeHandler, List flags) { + return buildResultMapping(resultType, property, column, javaType, jdbcType, nestedSelect, nestedResultMap, + notNullColumn, columnPrefix, typeHandler, flags, null, null, configuration.isLazyLoadingEnabled()); + } + + /** + * Gets the language driver. + * + * @param langClass + * the lang class + * + * @return the language driver + * + * @deprecated Use {@link Configuration#getLanguageDriver(Class)} + */ + @Deprecated + public LanguageDriver getLanguageDriver(Class langClass) { + return configuration.getLanguageDriver(langClass); + } + + private Set parseMultipleColumnNames(String columnName) { + Set columns = new HashSet<>(); + if (columnName != null) { + if (columnName.indexOf(',') > -1) { + StringTokenizer parser = new StringTokenizer(columnName, "{}, ", false); + while (parser.hasMoreTokens()) { + String column = parser.nextToken(); + columns.add(column); + } + } else { + columns.add(columnName); + } + } + return columns; + } + + private List parseCompositeColumnName(String columnName) { + List composites = new ArrayList<>(); + if (columnName != null && (columnName.indexOf('=') > -1 || columnName.indexOf(',') > -1)) { + StringTokenizer parser = new StringTokenizer(columnName, "{}=, ", false); + while (parser.hasMoreTokens()) { + String property = parser.nextToken(); + String column = parser.nextToken(); + ResultMapping complexResultMapping = new ResultMapping.Builder(configuration, property, column, + configuration.getTypeHandlerRegistry().getUnknownTypeHandler()).build(); + composites.add(complexResultMapping); + } + } + return composites; + } + + private Class resolveResultJavaType(Class resultType, String property, Class javaType) { + if (javaType == null && property != null) { + try { + MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory()); + javaType = metaResultType.getSetterType(property); + } catch (Exception e) { + // ignore, following null check statement will deal with the situation + } + } + if (javaType == null) { + javaType = Object.class; + } + return javaType; + } + + private Class resolveParameterJavaType(Class resultType, String property, Class javaType, + JdbcType jdbcType) { + if (javaType == null) { + if (JdbcType.CURSOR.equals(jdbcType)) { + javaType = ResultSet.class; + } else if (Map.class.isAssignableFrom(resultType)) { + javaType = Object.class; + } else { + MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory()); + javaType = metaResultType.getGetterType(property); + } + } + if (javaType == null) { + javaType = Object.class; + } + return javaType; + } + +}