记录一次 Mybatis Plus selectList的NullPointerException问题

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
        }
    }
}

在上述代码中,若查询结果里某一行的所有字段值都为nulluserList里对应的元素就会是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:表示在映射过程中是否找到了有效的数据。如果某一行的所有字段值都为 nullfoundValues 将为 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 时,由于 foundValuesfalseconfiguration.isReturnInstanceForEmptyRow()false,最终会返回 null 而不是一个对象实例。如果你希望在这种情况下返回一个对象实例,可以通过配置 returnInstanceForEmptyRowtrue 来实现。

这个问题出现的概率说实话还是挺低的,只要稍微注意一下就不会出现这种错误,奈何还是真遇到了。

如果对您有帮助 && 觉得我总结不错 => 受累点个免费的赞,这对我很重要,谢谢。

相关推荐
uhakadotcom2 分钟前
PyTorch 2.0:最全入门指南,轻松理解新特性和实用案例
后端·面试·github
bnnnnnnnn3 分钟前
前端实现多服务器文件 自动同步宝塔定时任务 + 同步工具 + 企业微信告警(实战详解)
前端·javascript·后端
DataFunTalk4 分钟前
乐信集团副总经理周道钰亲述 :乐信“黎曼”异动归因系统的演进之路
前端·后端·算法
DataFunTalk14 分钟前
开源一个MCP+数据库新玩法,网友直呼Text 2 SQL“有救了!”
前端·后端·算法
idMiFeng25 分钟前
通过GO后端项目实践理解DDD架构
后端
LemonDu37 分钟前
Cursor入门教程-JetBrains过度向
人工智能·后端
LTPP43 分钟前
掌握Rust Web开发的未来:Hyperlane框架全方位教程 🎓🔧
前端·后端·github
LemonDus1 小时前
Cursor入门教程-JetBrains过度向
后端·工具·技术知识
涡能增压发动积1 小时前
SpringAI+LiteFlow实现智能体编排
人工智能·后端
精神内耗中的钙奶饼干1 小时前
Windows 系统搭建Kafka集群记录
后端·kafka