在 MyBatis 项目中,一个常见问题是:当数据库查询没有匹配记录时,Mapper 方法返回什么? 是 null 还是空集合/空 Map?Java 代码中是否需要进行非空判断?

本文基于 MyBatis 3.5.x 系列(截至 2025 年 12 月行为一致)源码,详细分析单个对象、集合(List)、Map 三种常见返回类型的行为,并给出实践建议。

1. 核心结论

MyBatis 返回行为取决于 Mapper 方法的返回类型

返回类型 无记录时返回 有记录时返回 多条记录异常情况 是否需要 null 判断
单个对象(如 User) null 对应的对象实例 抛 TooManyResultsException 必须
List 空 List(size()==0) 包含元素的 List 无异常 不需要
Map<K,V> 空 Map(size()==0) 包含键值对的 Map 无异常 不需要

Java 代码实践

  • List 和 Map无需 null 判断,可以直接调用 isEmpty()、size()、forEach 等方法,绝对安全,不会 NPE。
  • 单个对象必须进行 null 判断

MyBatis 的集合和 Map 返回设计遵循 Java 最佳实践:返回空容器而非 null,极大减少 NPE 风险。

2. 源码深度分析

查询链路:Mapper 接口(动态代理) → DefaultSqlSession → Executor → DefaultResultSetHandler。

2.1 所有查询底层:selectList() → handleResultSets()

// DefaultSqlSession.selectList()
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}

最终结果处理在 DefaultResultSetHandler.handleResultSets():

public List<Object> handleResultSets(Statement stmt) throws SQLException {
    final List<Object> multipleResults = new ArrayList<>();  // 初始化空 List

    // 获取第一个 ResultSet
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    if (resultHandler == null) {  // 普通查询,resultHandler 为 null
        DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
        handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
        multipleResults.add(defaultResultHandler.getResultList());  // 添加结果(即使为空 List)
    }

    // 处理多 ResultSet 情况...
    return collapseSingleResultList(multipleResults);  // 始终返回 List(非 null)
}
  • 关键点:即使 ResultSet 没有一行数据(next() 为 false),DefaultResultHandler.getResultList() 仍返回 new ArrayList<>()(空 List)。
  • 对于 Map 返回类型,MyBatis 会使用 DefaultMapResultHandler(继承自 DefaultResultHandler),同样返回 new LinkedHashMap<>()(空 Map)。

2.2 selectOne() 的特殊处理(单个对象)

public <T> T selectOne(String statement, Object parameter) {
    List<T> list = selectList(statement, parameter);  // 复用 selectList
    if (list.size() == 0) {
        return null;  // 明确返回 null
    } else if (list.size() > 1) {
        throw new TooManyResultsException(...);
    }
    return list.get(0);
}

关键点:selectOne内部也是复用 selectList,且没有对list进行非空判断。

2.3 Map 返回的处理(DefaultMapResultHandler)

public class DefaultMapResultHandler<K, V> implements ResultHandler<V> {
    private final Map<K, V> map;
    public DefaultMapResultHandler() {
        this.map = new LinkedHashMap<>();  // 初始化空 Map
    }
    // ... 处理每一行
    public Map<K, V> getMappedResults() {
        return map;  // 即使无行,也返回空 Map
    }
}

3. 代码示例

public interface UserMapper {
    User getById(Long id);                          // 单个对象

    List<User> listByStatus(Integer status);        // List

    Map<Long, User> mapByIdIn(@Param("ids") List<Long> ids);  // Map<Long, User>
}
// 单个对象
User user = mapper.getById(999L);  // 无记录 → null
if (user != null) {  // 必须判断
    System.out.println(user.getName());
}

// List
List<User> users = mapper.listByStatus(999);  // 无记录 → 空 List
users.forEach(u -> ...);  // 安全
if (users.isEmpty()) {
    System.out.println("无数据");
}

// Map
Map<Long, User> userMap = mapper.mapByIdIn(emptyIdList);  // 无记录 → 空 Map
if (userMap.isEmpty()) {
    System.out.println("无数据");
}
userMap.forEach((id, u) -> ...);  // 安全迭代

4. 最佳实践与注意事项

  • 使用集合/Map 查询无需非空判断:如果没有查询到数据,MyBatis 会返回一个空的集合(ListSet等),不需要进行非空判断,只需要判断集合是否为空。
  • 单个对象场景:查询唯一键(如主键)时可用,但务必加 null 检查
  • 插件风险:自定义插件或 TypeHandler 可能改变返回行为,标准 MyBatis 保持上述规则。

5. 总结

MyBatis 在无记录时的返回行为清晰且安全:

  • List / Map:返回空容器(无需 null 判断)。
  • 单个对象:返回 null(必须判断)。

深入查看 DefaultResultSetHandler.java、DefaultMapResultHandler.java 和 DefaultSqlSession.java 源码,你会发现 MyBatis 在容器处理上非常严谨,始终避免返回 null。

掌握这些规则,能让你写出更少 bug、更易维护的持久层代码。

有特定返回类型或插件场景疑问,欢迎继续讨论!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐