MyBatis系列---crud返回值

1. service与mapper

mybatis一般与springboot联合使用,分为controller、service、mapper、domain四层,与mybatis相关的就是后三层。

但本质而言,service和mapper就是一层,service的存在就是对mapper进行了封装,跟踪源码就可以看出service层的各种方法其实还是调用了mapper层,mapper再去实现的数据库连接。

mapper接口

public interface BaseMapper<T> extends Mapper<T> {
    int insert(T entity);

    int deleteById(Serializable id);

    int deleteByMap(@Param("cm") Map<String, Object> columnMap);

    int delete(@Param("ew") Wrapper<T> wrapper);

    int deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList);

    int updateById(@Param("et") T entity);

    int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);

    T selectById(Serializable id);

    List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);

    List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);

    T selectOne(@Param("ew") Wrapper<T> queryWrapper);

    Integer selectCount(@Param("ew") Wrapper<T> queryWrapper);

    List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);

    List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);

    List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);

    IPage<T> selectPage(IPage<T> page, @Param("ew") Wrapper<T> queryWrapper);

    IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param("ew") Wrapper<T> queryWrapper);
}

// 虽然BaseMapper继承了Mapper,不过这只是mabitis的规范写法,Mapper接口是空的。所以BaseMapper中的方法就是mapper的所有方法了。包含了crud所有的操作。

IService接口

public interface IService<T> {
    boolean save(T entity);

    @Transactional(
        rollbackFor = {Exception.class}
    )
    default boolean saveBatch(Collection<T> entityList) {
        return this.saveBatch(entityList, 1000);
    }

    boolean saveBatch(Collection<T> entityList, int batchSize);

    @Transactional(
        rollbackFor = {Exception.class}
    )
    default boolean saveOrUpdateBatch(Collection<T> entityList) {
        return this.saveOrUpdateBatch(entityList, 1000);
    }

    boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);

    boolean removeById(Serializable id);

    boolean removeByMap(Map<String, Object> columnMap);

    boolean remove(Wrapper<T> queryWrapper);

    boolean removeByIds(Collection<? extends Serializable> idList);

    boolean updateById(T entity);

    boolean update(T entity, Wrapper<T> updateWrapper);

    default boolean update(Wrapper<T> updateWrapper) {
        return this.update((Object)null, updateWrapper);
    }

    @Transactional(
        rollbackFor = {Exception.class}
    )
    default boolean updateBatchById(Collection<T> entityList) {
        return this.updateBatchById(entityList, 1000);
    }

    boolean updateBatchById(Collection<T> entityList, int batchSize);

    boolean saveOrUpdate(T entity);

    T getById(Serializable id);

    Collection<T> listByIds(Collection<? extends Serializable> idList);

    Collection<T> listByMap(Map<String, Object> columnMap);

    default T getOne(Wrapper<T> queryWrapper) {
        return this.getOne(queryWrapper, true);
    }

    T getOne(Wrapper<T> queryWrapper, boolean throwEx);

    Map<String, Object> getMap(Wrapper<T> queryWrapper);

    <V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

    int count(Wrapper<T> queryWrapper);

    default int count() {
        return this.count(Wrappers.emptyWrapper());
    }

    List<T> list(Wrapper<T> queryWrapper);

    default List<T> list() {
        return this.list(Wrappers.emptyWrapper());
    }

    IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);

    default IPage<T> page(IPage<T> page) {
        return this.page(page, Wrappers.emptyWrapper());
    }

    List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);

    default List<Map<String, Object>> listMaps() {
        return this.listMaps(Wrappers.emptyWrapper());
    }

    default List<Object> listObjs() {
        return this.listObjs(Function.identity());
    }

    default <V> List<V> listObjs(Function<? super Object, V> mapper) {
        return this.listObjs(Wrappers.emptyWrapper(), mapper);
    }

    default List<Object> listObjs(Wrapper<T> queryWrapper) {
        return this.listObjs(queryWrapper, Function.identity());
    }

    <V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

    IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);

    default IPage<Map<String, Object>> pageMaps(IPage<T> page) {
        return this.pageMaps(page, Wrappers.emptyWrapper());
    }

    BaseMapper<T> getBaseMapper();

    default QueryChainWrapper<T> query() {
        return new QueryChainWrapper(this.getBaseMapper());
    }

    default LambdaQueryChainWrapper<T> lambdaQuery() {
        return new LambdaQueryChainWrapper(this.getBaseMapper());
    }

    default UpdateChainWrapper<T> update() {
        return new UpdateChainWrapper(this.getBaseMapper());
    }

    default LambdaUpdateChainWrapper<T> lambdaUpdate() {
        return new LambdaUpdateChainWrapper(this.getBaseMapper());
    }
}

ServiceImpl实现类

public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
    protected Log log = LogFactory.getLog(this.getClass());
    @Autowired
    protected M baseMapper;

    public ServiceImpl() {
    }

    public M getBaseMapper() {
        return this.baseMapper;
    }

    protected boolean retBool(Integer result) {
        return SqlHelper.retBool(result);
    }

    protected Class<T> currentModelClass() {
        return ReflectionKit.getSuperClassGenericType(this.getClass(), 1);
    }

    protected SqlSession sqlSessionBatch() {
        return SqlHelper.sqlSessionBatch(this.currentModelClass());
    }

    protected void closeSqlSession(SqlSession sqlSession) {
        SqlSessionUtils.closeSqlSession(sqlSession, GlobalConfigUtils.currentSessionFactory(this.currentModelClass()));
    }

    protected String sqlStatement(SqlMethod sqlMethod) {
        return SqlHelper.table(this.currentModelClass()).getSqlStatement(sqlMethod.getMethod());
    }

    public boolean save(T entity) {
        return this.retBool(this.baseMapper.insert(entity));
    }

    @Transactional(
        rollbackFor = {Exception.class}
    )
    public boolean saveBatch(Collection<T> entityList, int batchSize) {
        String sqlStatement = this.sqlStatement(SqlMethod.INSERT_ONE);
        SqlSession batchSqlSession = this.sqlSessionBatch();
        Throwable var5 = null;

        try {
            int i = 0;

            for(Iterator var7 = entityList.iterator(); var7.hasNext(); ++i) {
                T anEntityList = var7.next();
                batchSqlSession.insert(sqlStatement, anEntityList);
                if (i >= 1 && i % batchSize == 0) {
                    batchSqlSession.flushStatements();
                }
            }

            batchSqlSession.flushStatements();
            return true;
        } catch (Throwable var16) {
            var5 = var16;
            throw var16;
        } finally {
            if (batchSqlSession != null) {
                if (var5 != null) {
                    try {
                        batchSqlSession.close();
                    } catch (Throwable var15) {
                        var5.addSuppressed(var15);
                    }
                } else {
                    batchSqlSession.close();
                }
            }

        }
    }

    @Transactional(
        rollbackFor = {Exception.class}
    )
    public boolean saveOrUpdate(T entity) {
        if (null == entity) {
            return false;
        } else {
            Class<?> cls = entity.getClass();
            TableInfo tableInfo = TableInfoHelper.getTableInfo(cls);
            Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!", new Object[0]);
            String keyProperty = tableInfo.getKeyProperty();
            Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!", new Object[0]);
            Object idVal = ReflectionKit.getMethodValue(cls, entity, tableInfo.getKeyProperty());
            return !StringUtils.checkValNull(idVal) && !Objects.isNull(this.getById((Serializable)idVal)) ? this.updateById(entity) : this.save(entity);
        }
    }

    @Transactional(
        rollbackFor = {Exception.class}
    )
    public boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize) {
        Assert.notEmpty(entityList, "error: entityList must not be empty", new Object[0]);
        Class<?> cls = this.currentModelClass();
        TableInfo tableInfo = TableInfoHelper.getTableInfo(cls);
        Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!", new Object[0]);
        String keyProperty = tableInfo.getKeyProperty();
        Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!", new Object[0]);
        SqlSession batchSqlSession = this.sqlSessionBatch();
        Throwable var7 = null;

        try {
            int i = 0;

            for(Iterator var9 = entityList.iterator(); var9.hasNext(); ++i) {
                T entity = var9.next();
                Object idVal = ReflectionKit.getMethodValue(cls, entity, keyProperty);
                if (!StringUtils.checkValNull(idVal) && !Objects.isNull(this.getById((Serializable)idVal))) {
                    ParamMap<T> param = new ParamMap();
                    param.put("et", entity);
                    batchSqlSession.update(this.sqlStatement(SqlMethod.UPDATE_BY_ID), param);
                } else {
                    batchSqlSession.insert(this.sqlStatement(SqlMethod.INSERT_ONE), entity);
                }

                if (i >= 1 && i % batchSize == 0) {
                    batchSqlSession.flushStatements();
                }
            }

            batchSqlSession.flushStatements();
            return true;
        } catch (Throwable var20) {
            var7 = var20;
            throw var20;
        } finally {
            if (batchSqlSession != null) {
                if (var7 != null) {
                    try {
                        batchSqlSession.close();
                    } catch (Throwable var19) {
                        var7.addSuppressed(var19);
                    }
                } else {
                    batchSqlSession.close();
                }
            }

        }
    }

    public boolean removeById(Serializable id) {
        return SqlHelper.retBool(this.baseMapper.deleteById(id));
    }

    public boolean removeByMap(Map<String, Object> columnMap) {
        Assert.notEmpty(columnMap, "error: columnMap must not be empty", new Object[0]);
        return SqlHelper.retBool(this.baseMapper.deleteByMap(columnMap));
    }

    public boolean remove(Wrapper<T> wrapper) {
        return SqlHelper.retBool(this.baseMapper.delete(wrapper));
    }

    public boolean removeByIds(Collection<? extends Serializable> idList) {
        return SqlHelper.retBool(this.baseMapper.deleteBatchIds(idList));
    }

    public boolean updateById(T entity) {
        return this.retBool(this.baseMapper.updateById(entity));
    }

    public boolean update(T entity, Wrapper<T> updateWrapper) {
        return this.retBool(this.baseMapper.update(entity, updateWrapper));
    }

    @Transactional(
        rollbackFor = {Exception.class}
    )
    public boolean updateBatchById(Collection<T> entityList, int batchSize) {
        Assert.notEmpty(entityList, "error: entityList must not be empty", new Object[0]);
        String sqlStatement = this.sqlStatement(SqlMethod.UPDATE_BY_ID);
        SqlSession batchSqlSession = this.sqlSessionBatch();
        Throwable var5 = null;

        try {
            int i = 0;

            for(Iterator var7 = entityList.iterator(); var7.hasNext(); ++i) {
                T anEntityList = var7.next();
                ParamMap<T> param = new ParamMap();
                param.put("et", anEntityList);
                batchSqlSession.update(sqlStatement, param);
                if (i >= 1 && i % batchSize == 0) {
                    batchSqlSession.flushStatements();
                }
            }

            batchSqlSession.flushStatements();
            return true;
        } catch (Throwable var17) {
            var5 = var17;
            throw var17;
        } finally {
            if (batchSqlSession != null) {
                if (var5 != null) {
                    try {
                        batchSqlSession.close();
                    } catch (Throwable var16) {
                        var5.addSuppressed(var16);
                    }
                } else {
                    batchSqlSession.close();
                }
            }

        }
    }

    public T getById(Serializable id) {
        return this.baseMapper.selectById(id);
    }

    public Collection<T> listByIds(Collection<? extends Serializable> idList) {
        return this.baseMapper.selectBatchIds(idList);
    }

    public Collection<T> listByMap(Map<String, Object> columnMap) {
        return this.baseMapper.selectByMap(columnMap);
    }

    public T getOne(Wrapper<T> queryWrapper, boolean throwEx) {
        return throwEx ? this.baseMapper.selectOne(queryWrapper) : SqlHelper.getObject(this.log, this.baseMapper.selectList(queryWrapper));
    }

    public Map<String, Object> getMap(Wrapper<T> queryWrapper) {
        return (Map)SqlHelper.getObject(this.log, this.baseMapper.selectMaps(queryWrapper));
    }

    public int count(Wrapper<T> queryWrapper) {
        return SqlHelper.retCount(this.baseMapper.selectCount(queryWrapper));
    }

    public List<T> list(Wrapper<T> queryWrapper) {
        return this.baseMapper.selectList(queryWrapper);
    }

    public IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper) {
        return this.baseMapper.selectPage(page, queryWrapper);
    }

    public List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper) {
        return this.baseMapper.selectMaps(queryWrapper);
    }

    public <V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper) {
        return (List)this.baseMapper.selectObjs(queryWrapper).stream().filter(Objects::nonNull).map(mapper).collect(Collectors.toList());
    }

    public IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper) {
        return this.baseMapper.selectMapsPage(page, queryWrapper);
    }

    public <V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper) {
        return SqlHelper.getObject(this.log, this.listObjs(queryWrapper, mapper));
    }
}

由此就可以看出,service层的实现是依赖于mapper层的,能使用mapper的则使用了mapper,将返回值进行了封装。不能用mapper现成方法的则自己实现了。
部分特点如下:

  1. mapper的返回值一共有三种,分别是:int、list、bean
  2. service对返回值进行了优化,一共有四种:boolean、list、bean、map
  3. service增加了批量操作方法,也就是各种Batch操作,但是是伪批量,实际还是一条一条执行的
  4. service中的各种Batch操作是事务操作,使用了@Transactional注解修饰

2. 更新操作

数据库的数据操作分为查询和更新。查询语句用于各种检索操作,更新操作用于插入、删除和修改等操作。
所以本质来说,插入、删除、修改其实是一类操作,Create\Delete\Update。以create操作为示例:

service的返回值有两种true/false;对应mapper的1/0,service即使是saveBatch时返回值依旧是true/false。
此处我一直有一个疑问,那就是作为一个插入语句,究竟什么时候才会返回false,按理来说不是成功就是异常吗?如果没有报错并且也没有成功,真的有这种情况吗?到底咋触发呢

首先,true和false的返回值其实是根据mysql的返回值:Affected rows来判断的?只要返回值符合插入的条数就是ture,不符合条数则是false,如果mysql返回值没有这个字段,则抛异常,输出具体的错误信息。

#成功#
在这里插入图片描述

#异常#
在这里插入图片描述
在这里插入图片描述

所以,对于Create操作,出于严谨考虑,判断返回值还是很有必要的,尤其是正常业务中,插入失败和抛异常其实应该是同一个处理逻辑,但是失败是不会主动触发异常的,因此必须监控save的返回值,主动抛出异常。(当然,如果一个方法中有很多save语句,每个都判断确实很烦,所以省略也不是不行,因为false的几率真的很低很低)
示例代码:

Boolean mres1 = iJmlMoneyflowingService.save(scanMoneyFlow);
Boolean rres1 = icommonService.setLasterMoneyFlowing(openid,scanMoneyFlow);
if(!mres1 || !rres1 ){
     logger.info("扫码领取红包异常1");
     throw new Exception();
}

再说对于Delete\Update操作,其实与Create是同一个原理,但此时对于返回值的判断则不能是嫌麻烦可以不判断,而是必须判断返回值,因为对于更新和删除操作一般都会有where条件进行判断,经常用的的就是数据库计算,因此对于返回值的判断是必须的。
示例代码:

// 被申诉人更新核销数据汇总
Boolean mres1 = iJmlOnenjoyamountService.update(new UpdateWrapper<JmlOnenjoyamount>().setSql("amount = amount - " + getNumber).set("update_time", time).eq("user_id", sourceId).eq("project_code", getSku));
// 申诉人更新核销数据汇总	
Boolean mres2 = iJmlOnenjoyamountService.update(new UpdateWrapper<JmlOnenjoyamount>().setSql("amount = amount + " + getNumber).set("update_time", time).eq("user_id", userId).eq("project_code", getSku));
if(!mres1 || !rres1 ){
    logger.info("核销异常");
    throw new Exception();
}        

综上,更新类型的操作均需要判断返回值,只有判断了返回值才是最严谨的操作。(但是我发现公司的项目基本都是没判断的,期待后续的深入学习吧!)

3. 查询操作

查询操作对应的就是Retrieve,对于mybatis来说,返回值有三种,bean,list,map。使用这三种时真的应该好好校验,因为有可能是null,有可能是空。下面就来看看为什么!

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;
它有两个实现类:

  1. DefaultResultHandler 实现的底层使用 ArrayList 存储单个结果集映射得到的 Java 对象列表。

  2. DefaultMapResultHandler 实现的底层使用 Map<K, V> 存储映射得到的 Java 对象,其中 Key 是从结果对象中获取的指定属性的值,Value 就是映射得到的 Java 对象。

DefaultResultContext 对象,它的生命周期与一个 ResultSet 相同,每从 ResultSet 映射得到一个 Java 对象都会暂存到 DefaultResultContext 中的 resultObject 字段,等待后续使用。
同时 DefaultResultContext 还可以计算从一个 ResultSet 映射出来的对象个数(依靠 resultCount 字段统计)。

3.1. 返回值存储

数据库支持同时返回多个 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 的。

3.2. 简单映射

DefaultResultSetHandler 是如何处理单个结果集的,这部分逻辑的入口是 handleResultSet() 方法,其中会根据第四个参数,也就是 parentMapping,判断当前要处理的 ResultSet 是嵌套映射,还是外层映射。

无论是处理外层映射还是嵌套映射,都会依赖 handleRowValues() 方法完成结果集的处理。

通过方法名也可以看出,handleRowValues() 方法是处理多行记录的,也就是一个结果集。

handleRowValuesForNestedResultMap() 方法处理包含嵌套映射的 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);
    }
}

该方法的核心步骤可总结为如下:

  1. 执行 skipRows() 方法跳过多余的记录,定位到指定的行。

  2. 通过 shouldProcessMoreRows() 方法,检测是否还有需要映射的数据记录。

  3. 如果存在需要映射的记录,则先通过 resolveDiscriminatedResultMap() 方法处理映射中用到的 Discriminator,决定此次映射实际使用的 ResultMap。

  4. 通过 getRowValue() 方法对 ResultSet 中的一行记录进行映射,映射规则使用的就是步骤 3 中确定的 ResultMap。

  5. 执行 storeObject() 方法记录步骤 4 中返回的、映射好的 Java 对象。

3.3. ResultSet 的预处理

我们可以通过 RowBounds 指定 offset、limit 参数实现分页的效果。

这里的 skipRows() 方法就会根据 RowBounds 移动 ResultSet 的指针到指定的数据行,这样后续的映射操作就可以从这一行开始。

通过上述分析我们可以看出,通过 RowBounds 实现的分页功能实际上还是会将全部数据加载到 ResultSet 中,而不是只加载指定范围的数据所以我们可以认为 RowBounds 实现的是一种“假分页”。

这种“假分页”在数据量大的时候,性能就会很差,在处理大数据量分页时,建议通过 SQL 语句 where 条件 + limit 的方式实现分页。

3.4. 确定 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() 方法去了解一下,这里不再赘述。

3.5. 创建映射结果对象

确定了当前记录使用哪个 ResultMap 进行映射之后,要做的就是按照 ResultMap 规则进行各个列的映射,得到最终的 Java 对象,这部分逻辑是在 getRowValue() 方法完成的。

其核心步骤如下:

  1. 首先根据 ResultMap 的 type 属性值创建映射的结果对象。

  2. 然后根据 ResultMap 的配置以及全局信息,决定是否自动映射 ResultMap 中未明确映射的列。

  3. 接着根据 ResultMap 映射规则,将 ResultSet 中的列值与结果对象中的属性值进行映射。

  4. 最后返回映射的结果对象,如果没有映射任何属性,则需要根据全局配置决定如何返回这个结果值,这里不同场景和配置,可能返回完整的结果对象、空结果对象或是 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;
}

3.6. 自动映射

创建完结果对象之后,下面就可以开始映射各个字段了。在简单映射流程中,会先通过 shouldApplyAutomaticMappings() 方法检测是否开启了自动映射。

主要检测以下两个地方:

  1. 检测当前使用的 ResultMap 是否配置了 autoMapping 属性,如果是,则直接根据该 autoMapping 属性的值决定是否开启自动映射功能。

  2. 检测 mybatis-config.xml 的 标签中配置的 autoMappingBehavior 值,决定是否开启自动映射功能。NONE 表示关闭自动映射;PARTIAL 只会自动映射没有定义嵌套结果映射的字段;FULL 会自动映射任何复杂的结果集(无论是否嵌套)。

完成自动映射之后,MyBatis 会执行 applyPropertyMappings() 方法处理 ResultMap 中明确要映射的列。

3.7. 存储对象

通过上述 6 个步骤,我们已经完成简单映射的处理,得到了一个完整的结果对象。接下来,我们就要通过 storeObject() 方法把这个结果对象保存到合适的位置。

private void storeObject(...) throws SQLException {
    if (parentMapping != null) {
        // 嵌套查询或嵌套映射的场景,此时需要将结果对象保存到外层对象对应的属性中
        linkToParents(rs, parentMapping, rowValue);
    } else {
        // 普通映射(没有嵌套映射)或是嵌套映射中的外层映射的场景,此时需要将结果对象保存到ResultHandler中
        callResultHandler(resultHandler, resultContext, rowValue);
    }
}

这里处理的简单映射,如果是一个嵌套映射中的子映射,那么我们就需要将结果对象保存到外层对象的属性中。

如果是一个普通映射或是外层映射的结果对象,那么我们就需要将结果对象保存到 ResultHandler 中。

3.8. 返回结果为单行数据

可以从 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;
    }
  }

3.9. 返回结果为多行数据

那么我们看到 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();
    }
}

3.10. 结论

看到这里相信已经完全明白了mybatis的返回值,为什么有时null,有时空list。

其实不管你是查单行记录还是多行记录,对于 Mybatis 来说都会放到 DefaultResultHandler 中去,而 DefaultResultHandler 又是用 List 存储结果。

所以不管是集合类型还是普通对象,Mybatis 都会先初始化一个 List 存储结果,然后返回值为普通对象且查为空的时候,selectOne 会判断然后直接返回 NULL 值。

而返回值为集合对象且查为空时,selectList 会把这个存储结果的 List 对象直接返回,此时这个 List 就是个空集合。

汇总如下:

  1. 查询时,如果查询的返回类型是普通Bean,若查询无结果,则返回null
  2. 查询时,如果查询的返回类型是List,若查询无结果,则返回空List对象
  3. 查询时,如果查询的返回类型是Map,若查询无结果,则返回空Map对象

但是,其实搞清楚mybatis查询时返回值这个问题前后挺浪费时间的,而且结论也很有局限性,所以养成一个良好的开发习惯其实可以避免耗费这么多时间,无论是mysql还是其他的地方取到的数据,比如redis、kafka,甚至是其他人的接口,都需要对返回值进行检验 ,并且是双重校验null及空。
当然如果是自己的方法一定要注意不要返回null,这也是开发习惯问题。
代码示例:

// 对于返回值为bean
JmlFlowing sourceLaster = iCommonService.getLasterFlowing(sourceId);
if (sourceLaster == null) {
    logger.info("被交易人流水异常");
    return new ResponseBean<>("402", "被扫码人流水异常", "被扫码人流水异常");
}
// 对于返回值为List
List<JmlPrize> prizes = iJmlPrizeService.list();
if (prizes == null || prizes.isEmpty()) {
    prizes = Collections.emptyList();
    logger.warn("奖品列表异常,{}",prizes);
}
// 对于返回值为map
Map<String,String> res = iJmlPrizeService.getMap(queryWrapper);
if (res == null || res.isEmpty()) {
    prizes = Collections.emptyMap();
    logger.warn("返回值异常,{}",res);
}

因此关于返回值一定要养成好的开发习惯:

  1. 对第三方返回值做严格的校验,null值判断,空值判断!
  2. 自己写的接口一定要优化返回值,不要返回null,可以返回空,这样自己的方法调用时可以省去很多空指针异常。
  3. 优化日志输出,对于第三方的返回值进行校验可以输出很多有意义的异常值。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lipviolet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值