Mybatis Plus selectList 问题
问题描述
上周调查询接口,突然报了个空指针,我看了看同事写的后端代码,发现是在用 MyBatis Plus 的 mapper接口调用 selectList 方法查询数据时,抛出空指针异常!调试发现,返回的列表里有些元素是 null,在后续的操作中使用列表中的对象导致了空指针。其实在操作过程中的对象,哪怕属性全是 null,只要对象不是null也不会报这个错误。
今天正好有时间,总结一下。如果对您有帮助 && 觉得我总结不错 => 受累点个免费的赞,这对我很重要。
示例说明
打个比方,公司的业务数据就不展示了,效果是一样的,假设存在SQL (select * from user where ...)查询结果如下
ID | NAME | AGE |
---|---|---|
001 | null | null |
代码示例,我只查NAME、AGE,不查ID
java
// 实体
public class User {
private Long id;
private String name;
private Integer age;
// getter setter
}
// Mapper接口
@Mapper
@Repository
interface UserMapper extends BaseMapper<User> {
}
// 测试代码
public class Main {
public static void main(String[] args) {
UserMapper userMapper = null; // 这里需要实际注入Mapper
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("name","age"); // 这个地方没查询ID,导致整个返回结果的每个字段都是null
List<User> userList = userMapper.selectList(queryWrapper);
for (User user : userList) {
System.out.println(user); // null
}
}
}
在上述代码中,若查询结果里某一行的所有字段值都为null
,userList
里对应的元素就会是null
,后续如果操作userList
中的对象就会报空指针异常。
原因分析
核心处理流程
MyBatis Plus本质上是基于MyBatis开发的,在MyBatis处理查询结果时,若一行数据里所有字段都是null
,MyBatis会认为没有有效数据可映射到目标对象,所以不会创建该对象,而是直接返回null
。这样做是为了避免创建无实际意义的对象,从而减少内存开销。
MyBatis在处理查询结果时,会经过一系列步骤,其中关键的步骤是结果集映射 。在这个过程中,MyBatis会尝试将数据库查询结果集中的每一行数据映射到Java对象上。当某一行的所有字段值都为null
时,MyBatis认为没有有效的数据可以映射到目标对象,因此不会创建该对象实例。
源码分析
1. ResultSetHandler
接口
ResultSetHandler
接口负责处理数据库查询返回的结果集。MyBatis 提供了默认的实现类 DefaultResultSetHandler
。
2. DefaultResultSetHandler
类
在 DefaultResultSetHandler
类中,handleRowValues
方法用于处理结果集的每一行数据。以下是简化后的关键代码逻辑:
java
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<>();
skipRows(rsw.getResultSet(), rowBounds);
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
Object rowValue = getRowValue(rsw, discriminatedResultMap);
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
}
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
3. 关键逻辑分析
在 getRowValue
方法中,以下代码行是决定是否返回 null
的关键:
java
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
foundValues
:表示在映射过程中是否找到了有效的数据。如果某一行的所有字段值都为null
,foundValues
将为false
。configuration.isReturnInstanceForEmptyRow()
:这是一个配置项,默认值为false
。如果将其设置为true
,即使没有找到有效的数据,也会返回一个对象实例。
解决办法
1、手动过滤null
替换成一个新的对象实例,stream.filter(Objects::nonNull)
同理:
java
import java.util.ArrayList;
import java.util.List;
// ... 前面的实体类和Mapper接口代码 ...
// 测试代码
public class Main {
public static void main(String[] args) {
UserMapper userMapper = null; // 这里需要实际注入Mapper
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
List<User> userList = userMapper.selectList(queryWrapper);
List<User> resultList = new ArrayList<>();
for (User user : userList) {
if (user == null) {
resultList.add(new User());
} else {
resultList.add(user);
}
}
for (User user : resultList) {
System.out.println(user);
}
}
}
通过这种方式,就能保证返回的列表里不会有null
元素,而是所有属性值为null
的对象。
2、配置方式
- 在 MyBatis 的配置文件中添加如下配置:
xml
<settings>
<setting name="returnInstanceForEmptyRow" value="true"/>
</settings>
- 在 application.yaml中配置:
yaml
mybatis-plus:
configuration:
return-instance-for-empty-row: true
通过上述配置,MyBatis Plus在处理全为 null
的行时,会返回一个字段都是null的对象实例,而不是 null
。
3、编程习惯
在编码过程中注意SQL的返回值,在select中尽量查询一个必不为空的字段例如主键,比如我要把ID也查出来的就不会有这个问题了。
如果项目已经跑了很长时间了,建议不要改配置文件,谁知道会不会影响其他的代码,另一方面要避免影响性能。
问题总结
MyBatis 默认情况下,当某一行的所有字段值都为 null
时,由于 foundValues
为 false
且 configuration.isReturnInstanceForEmptyRow()
为 false
,最终会返回 null
而不是一个对象实例。如果你希望在这种情况下返回一个对象实例,可以通过配置 returnInstanceForEmptyRow
为 true
来实现。
这个问题出现的概率说实话还是挺低的,只要稍微注意一下就不会出现这种错误,奈何还是真遇到了。
如果对您有帮助 && 觉得我总结不错 => 受累点个免费的赞,这对我很重要,谢谢。