背景
一行数据记录如何映射成一个 Java 对象,这种映射机制是 MyBatis 作为 ORM 框架的外围性能之一,也是咱们这篇文章须要学习的内容。
开始前咱们先看一个问题:
你是否已经在学习 Mybatis 的时候跟我有一样的疑难,什么状况下返回 null,什么时候是空集合,为什么会是这种后果?那么你感觉上述这种答复能压服你嘛?
我想应该不能吧,除非亲眼所见,否则真的很难确认他人说的是对还是错(毕竟网上的答案真的千奇百怪,啥都有,曾经不是第一次发现一些谬误的说法被宽泛流传了),那么这篇文章咱们就简略的剖析一下。
看完这篇你就晓得查问后果为空时候为什么汇合会是空集合而不是 NULL,而对象为什么会是 NULL 了。
PS:对过程不感兴趣的能够间接跳到最初看论断。
JDBC 中的 ResultSet 简介
你如果有 JDBC 编程教训的话,应该晓得在数据库中执行一条 Select 语句通常只能拿到一个 ResultSet,而后果集 ResultSet 是数据中查问后果返回的一种对象,能够说后果集是一个存储查问后果的对象。
然而后果集并不仅仅具备存储的性能,他同时还具备操纵数据的性能,可能实现对数据的更新等,咱们能够通过 next() 办法将指针挪动到下一行记录,而后通过 getXX() 办法来获取值。
while(rs.next()){ // 获取数据 int id = rs.getInt(1); String name = rs.getString("name"); System.out.println(id + "---" + name);}
后果集解决入口 ResultSetHandler
当 MyBatis 执行完一条 select 语句,拿到 ResultSet 后果集之后,会将其交给关联的 ResultSetHandler 进行后续的映射解决。
在 MyBatis 中只提供了一个 ResultSetHandler 接口实现,即 DefaultResultSetHandler。
上面咱们就以 DefaultResultSetHandler 为核心,介绍 MyBatis 中 ResultSet 映射的外围流程。
它的构造如下:
public interface ResultSetHandler { // 将ResultSet映射成Java对象 <E> List<E> handleResultSets(Statement stmt) throws SQLException; // 将ResultSet映射成游标对象 <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException; // 解决存储过程的输入参数 void handleOutputParameters(CallableStatement cs) throws SQLException;}
| handleResultSets
DefaultResultSetHandler 实现的 handleResultSets() 办法就反对多个 ResultSet 的解决,外面所调用的 handleResultSet() 办法就是负责解决单个 ResultSet。
通过 while 循环来实现多个 ResultSet 的解决:
public List<Object> handleResultSets(Statement stmt) throws SQLException { // 用于记录每个ResultSet映射进去的Java对象 final List<Object> multipleResults = new ArrayList<>(); int resultSetCount = 0; // 从Statement中获取第一个ResultSet,其中对不同的数据库有兼容解决逻辑, // 这里拿到的ResultSet会被封装成ResultSetWrapper对象返回 ResultSetWrapper rsw = getFirstResultSet(stmt); // 获取这条SQL语句关联的全副ResultMap规定。如果一条SQL语句可能产生多个ResultSet, // 那么在编写Mapper.xml映射文件的时候,咱们能够在SQL标签的resultMap属性中配置多个 // <resultMap>标签的id,它们之间通过","分隔,实现对多个后果集的映射 List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); while (rsw != null && resultMapCount > resultSetCount) { // 遍历ResultMap汇合 ResultMap resultMap = resultMaps.get(resultSetCount); // 依据ResultMap中定义的映射规定解决ResultSet,并将映射失去的Java对象增加到 // multipleResults汇合中保留 handleResultSet(rsw, resultMap, multipleResults, null); // 获取下一个ResultSet rsw = getNextResultSet(stmt); // 清理nestedResultObjects汇合,这个汇合是用来存储两头数据的 cleanUpAfterHandlingResultSet(); resultSetCount++; // 递增ResultSet编号 } // 上面这段逻辑是依据ResultSet的名称解决嵌套映射,你能够临时不关注这段代码, // 嵌套映射会在前面具体介绍 ... // 返回全副映射失去的Java对象 return collapseSingleResultList(multipleResults);}private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException { try { if (parentMapping != null) { handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping); } else { if (resultHandler == null) { DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory); handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null); // 将该ResultSet后果集解决完后的List对象放入multipleResults中,这样就能够反对返回多个后果集了 multipleResults.add(defaultResultHandler.getResultList()); } else { handleRowValues(rsw, resultMap, resultHandler, rowBounds, null); } } } finally { // issue #228 (close resultsets) closeResultSet(rsw.getResultSet()); }}
这里获取到的 ResultSet 对象,会被包装成 ResultSetWrapper 对象,而 ResultSetWrapper 次要用于封装 ResultSet 的一些元数据,其中记录了 ResultSet 中每列的名称、对应的 Java 类型、JdbcType 类型以及每列对应的 TypeHandler。
| DefaultResultHandler 和 DefaultResultContext
在开始具体介绍映射流程中的每一步之前,咱们先来看一下贯通整个映射过程的两个辅助对象 DefaultResultHandler 和 DefaultResultContext。
在 DefaultResultSetHandler 中保护了一个 resultHandler 字段(ResultHandler 接口类型),它默认状况下为空。
比方 DefaultSqlSession#selectList() 中传递的值就是 ResultHandler NO_RESULT_HANDLER = null;
它有两个实现类:
- DefaultResultHandler 实现的底层应用 ArrayList<Object> 存储单个后果集映射失去的 Java 对象列表。
- DefaultMapResultHandler 实现的底层应用 Map<K, V> 存储映射失去的 Java 对象,其中 Key 是从后果对象中获取的指定属性的值,Value 就是映射失去的 Java 对象。
DefaultResultContext 对象,它的生命周期与一个 ResultSet 雷同,每从 ResultSet 映射失去一个 Java 对象都会暂存到 DefaultResultContext 中的 resultObject 字段,期待后续应用。
同时 DefaultResultContext 还能够计算从一个 ResultSet 映射进去的对象个数(依附 resultCount 字段统计)。
| 多后果集返回
数据库反对同时返回多个 ResultSet 的场景,例如在存储过程中执行多条 Select 语句。
MyBatis 作为一个通用的长久化框架,不仅要反对罕用的根底性能,还要对其余应用场景进行全面的反对。
而反对多后果集返回的逻辑就在 collapseSingleResultList 办法中:
private List<Object> collapseSingleResultList(List<Object> multipleResults) { // 如果只有一个后果集就返回一个,否则间接通过List列表返回多个后果集 return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;}
multipleResults 里有多少个 List 列表取决于 handleResultSet() 办法里的 resultHandler == null 的判断。
默认状况下没有设置 resultHandler 的话,那每解决一个 ResultSet 就会增加后果到 multipleResults 中, 此时 multipleResults.size() == 1 必然是不等于 1 的。
注:感兴趣的能够自行查看 resultHandler 什么时候会不为空。
简略映射
DefaultResultSetHandler 是如何解决单个后果集的,这部分逻辑的入口是 handleResultSet() 办法,其中会依据第四个参数,也就是 parentMapping,判断以后要解决的 ResultSet 是嵌套映射,还是外层映射。
无论是解决外层映射还是嵌套映射,都会依赖 handleRowValues() 办法实现后果集的解决。
通过办法名也能够看出,handleRowValues() 办法是解决多行记录的,也就是一个后果集。
handleRowValuesForNestedResultMap() 办法解决蕴含嵌套映射的 ResultMap,是否为嵌套查问后果集,看 <resultMap> 申明时,是否蕴含 association、collection、case 关键字。
handleRowValuesForSimpleResultMap() 办法解决不蕴含嵌套映射的简略 ResultMap。
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { if (resultMap.hasNestedResultMaps()) { // 蕴含嵌套映射的解决流程 ensureNoRowBounds(); checkResultHandler(); handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping); } else { // 简略映射的解决 handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping); }}private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { DefaultResultContext<Object> resultContext = new DefaultResultContext<>(); ResultSet resultSet = rsw.getResultSet(); // 跳过多余的记录 skipRows(resultSet, rowBounds); // 检测是否还有须要映射的数据 while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { // 解决映射中用到的 Discriminator,决定此次映射理论应用的 ResultMap。 ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); Object rowValue = getRowValue(rsw, discriminatedResultMap, null); storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); }}
该办法的外围步骤可总结为如下:
- 执行 skipRows() 办法跳过多余的记录,定位到指定的行。
- 通过 shouldProcessMoreRows() 办法,检测是否还有须要映射的数据记录。
- 如果存在须要映射的记录,则先通过 resolveDiscriminatedResultMap() 办法解决映射中用到的 Discriminator,决定此次映射理论应用的 ResultMap。
- 通过 getRowValue() 办法对 ResultSet 中的一行记录进行映射,映射规定应用的就是步骤 3 中确定的 ResultMap。
- 执行 storeObject() 办法记录步骤 4 中返回的、映射好的 Java 对象。
| ResultSet 的预处理
咱们能够通过 RowBounds 指定 offset、limit 参数实现分页的成果。
这里的 skipRows() 办法就会依据 RowBounds 挪动 ResultSet 的指针到指定的数据行,这样后续的映射操作就能够从这一行开始。
通过上述剖析咱们能够看出,通过 RowBounds 实现的分页性能实际上还是会将全副数据加载到 ResultSet 中,而不是只加载指定范畴的数据所以咱们能够认为 RowBounds 实现的是一种“假分页”。
这种“假分页”在数据量大的时候,性能就会很差,在解决大数据量分页时,倡议通过 SQL 语句 where 条件 + limit 的形式实现分页。
| 确定 ResultMap
在实现 ResultSet 的预处理之后,接下来会通过 resolveDiscriminatedResultMap() 办法解决标签,确定此次映射操作最终应用的 ResultMap 对象。
public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix) throws SQLException { // 用于保护解决过的ResultMap惟一标识 Set<String> pastDiscriminators = new HashSet<>(); // 获取ResultMap中的Discriminator对象,这是通过<resultMap>标签中的<discriminator>标签解析失去的 Discriminator discriminator = resultMap.getDiscriminator(); while (discriminator != null) { // 获取以后待映射的记录中Discriminator要检测的列的值 final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix); // 根据上述列值确定要应用的ResultMap的惟一标识 final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value)); if (configuration.hasResultMap(discriminatedMapId)) { // 从全局配置对象Configuration中获取ResultMap对象 resultMap = configuration.getResultMap(discriminatedMapId); // 记录以后Discriminator对象 Discriminator lastDiscriminator = discriminator; // 获取ResultMap对象中的Discriminator discriminator = resultMap.getDiscriminator(); // 检测Discriminator是否呈现了环形援用 if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) { break; } } else { break; } } // 返回最终要应用的ResultMap return resultMap;}
至于 ResultMap 对象是怎么创立的,感兴趣的能够自行从 XMLMapperBuilder#resultMapElements() 办法去理解一下,这里不再赘述。
| 创立映射后果对象
确定了以后记录应用哪个 ResultMap 进行映射之后,要做的就是依照 ResultMap 规定进行各个列的映射,失去最终的 Java 对象,这部分逻辑是在 getRowValue() 办法实现的。
其外围步骤如下:
- 首先依据 ResultMap 的 type 属性值创立映射的后果对象。
- 而后依据 ResultMap 的配置以及全局信息,决定是否主动映射 ResultMap 中未明确映射的列。
- 接着依据 ResultMap 映射规定,将 ResultSet 中的列值与后果对象中的属性值进行映射。
- 最初返回映射的后果对象,如果没有映射任何属性,则须要依据全局配置决定如何返回这个后果值,这里不同场景和配置,可能返回残缺的后果对象、空后果对象或是 null。
这个能够关注 mybatis 配置中的 returnInstanceForEmptyRow 属性,它默认为 false。
当返回行的所有列都是空时,MyBatis 默认返回 null。当开启这个设置时,MyBatis会返回一个空实例。
请留神,它也实用于嵌套的后果集(如汇合或关联)。(新增于 3.4.2)
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException { final ResultLoaderMap lazyLoader = new ResultLoaderMap(); // 依据ResultMap的type属性值创立映射的后果对象 Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { final MetaObject metaObject = configuration.newMetaObject(rowValue); boolean foundValues = this.useConstructorMappings; // 依据ResultMap的配置以及全局信息,决定是否主动映射ResultMap中未明确映射的列 if (shouldApplyAutomaticMappings(resultMap, false)) { foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues; } // 依据ResultMap映射规定,将ResultSet中的列值与后果对象中的属性值进行映射 foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues; // 如果没有映射任何属性,须要依据全局配置决定如何返回这个后果值, // 这里不同场景和配置,可能返回残缺的后果对象、空后果对象或是null foundValues = lazyLoader.size() > 0 || foundValues; rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null; } return rowValue;}
| 主动映射
创立完后果对象之后,上面就能够开始映射各个字段了。在简略映射流程中,会先通过 shouldApplyAutomaticMappings() 办法检测是否开启了主动映射。
次要检测以下两个中央:
- 检测以后应用的 ResultMap 是否配置了 autoMapping 属性,如果是,则间接依据该 autoMapping 属性的值决定是否开启主动映射性能。
- 检测 mybatis-config.xml 的 <settings> 标签中配置的 autoMappingBehavior 值,决定是否开启主动映射性能。NONE 示意敞开主动映射;PARTIAL 只会主动映射没有定义嵌套后果映射的字段;FULL 会主动映射任何简单的后果集(无论是否嵌套)。
| 失常映射
实现主动映射之后,MyBatis 会执行 applyPropertyMappings() 办法解决 ResultMap 中明确要映射的列。
| 存储对象
通过上述 5 个步骤,咱们曾经实现简略映射的解决,失去了一个残缺的后果对象。
接下来,咱们就要通过 storeObject() 办法把这个后果对象保留到适合的地位。
private void storeObject(...) throws SQLException { if (parentMapping != null) { // 嵌套查问或嵌套映射的场景,此时须要将后果对象保留到外层对象对应的属性中 linkToParents(rs, parentMapping, rowValue); } else { // 一般映射(没有嵌套映射)或是嵌套映射中的外层映射的场景,此时须要将后果对象保留到ResultHandler中 callResultHandler(resultHandler, resultContext, rowValue); }}
这里解决的简略映射,如果是一个嵌套映射中的子映射,那么咱们就须要将后果对象保留到外层对象的属性中。
如果是一个一般映射或是外层映射的后果对象,那么咱们就须要将后果对象保留到 ResultHandler 中。
回归最后的问题:查问后果为空时的返回值
| 返回后果为单行数据
能够从 ResultSetHandler的handleResultSets 办法开始剖析。
multipleResults 用于记录每个 ResultSet 映射进去的 Java 对象,留神这里是每个 ResultSet,也就说能够有多个后果集。
咱们能够看到 DefaultSqlSession#selectOne() 办法,咱们先说论断:因为只有一个 ResultSet 后果集,那么返回值为 null。
步骤如下:
handleResultSet() 办法的 handleRowValuesForSimpleResultMap 会判断 ResultSet.next,此时为 false,间接跳过(遗记了的,返回去看简略映射章节)
// 检测是否还有须要映射的数据 while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next())
而后 multipleResults.add(defaultResultHandler.getResultList());中取得的 list 就是默认创立的空集合。
public class DefaultResultHandler implements ResultHandler<Object> { // 默认是空集合 private final List<Object> list; public DefaultResultHandler() { list = new ArrayList<>(); } @SuppressWarnings("unchecked") public DefaultResultHandler(ObjectFactory objectFactory) { list = objectFactory.create(List.class); } @Override public void handleResult(ResultContext<? extends Object> context) { list.add(context.getResultObject()); } public List<Object> getResultList() { return list; }}
接下来 selectOne 拿到的就是空 list,此时 list.size() == 1和list.size() > 1 均为 false,所以它的返回值为 NULL。
public <T> T selectOne(String statement, Object parameter) { // Popular vote was to return null on 0 results and throw exception on too many. List<T> list = this.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; } }
| 返回后果为多行数据
那么咱们看到 DefaultSqlSession#selectList() 办法,先说论断:返回值为空集合而不是 NULL。
后面都同理,感兴趣的能够本人顺着 executor.query 一路往下看,会发现最初就是调用的 resultSetHandler.handleResultSets() 办法。
只不过 selectList 是间接把 executor.query 从 defaultResultHandler.getResultList() 返回的空集合没有做解决,间接返回。
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); }}
论断
看到这,咱们在反过来看下面截图里的答案,什么返回值是 Java 汇合会先初始化??而且如果是 Map 作为返回值的话,那间接是返回的 NULL 好吧,几乎是错的离谱!
如果返回值是 Java 汇合类型,如 List、Map,会先初始化(new 一个汇合对象),再把后果增加进去;如果返回值是一般对象,查问不到时,返回值是 null。
其实不论你是查单行记录还是多行记录,对于 Mybatis 来说都会放到 DefaultResultHandler 中去,而 DefaultResultHandler 又是用 List 存储后果。
所以不论是汇合类型还是一般对象,Mybatis 都会先初始化一个 List 存储后果,而后返回值为一般对象且查为空的时候,selectOne 会判断而后间接返回 NULL 值。
而返回值为汇合对象且查为空时,selectList 会把这个存储后果的 List 对象间接返回,此时这个 List 就是个空集合。
起源:https://c1n.cn/6l7NH