MyBatis系列---crud返回值

news2024/11/26 2:36:32

目录

  • 1. service与mapper
  • 2. 更新操作
  • 3. 查询操作
    • 3.1. 返回值存储
    • 3.2. 简单映射
    • 3.3. ResultSet 的预处理
    • 3.4. 确定 ResultMap
    • 3.5. 创建映射结果对象
    • 3.6. 自动映射
    • 3.7. 存储对象
    • 3.8. 返回结果为单行数据
    • 3.9. 返回结果为多行数据
    • 3.10. 结论

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. 优化日志输出,对于第三方的返回值进行校验可以输出很多有意义的异常值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/83996.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度活体模型带交互模型版

🍿*★,*:.☆欢迎您/$:*.★* 🍿

点击Tab标签切换不同查询数据,并选择数据存入缓存实现两个界面带参数跳转

项目场景&#xff1a; 在不同的tab标签页中点击不同的标签页查找不同的内容,然后选中其中一个页面中的一条数据将此数据某个信息选中然后存入session缓存当中然后另一个界面从session中取出,从而达到带参数跳转界面的需求 问题描述 可以做到跳转界面但是数据会显示到地址栏当…

做开发4年了,年薪还不如2年经验的测试。我该适应当下节奏吗...

代码码了这么些年&#xff0c;你年薪达到多少了&#xff1f; 我&#xff0c;4年码龄&#xff0c;薪资最高的时候16k*12薪&#xff0c;年薪不到20W。都说IT行业薪资高&#xff0c;但年薪百万的还是金字塔尖极少数&#xff0c;像我这样的才是普通的大多数&#xff0c;却也还要用…

电脑维护与故障处理

第一章 认识电脑的组成 1.1 硬件组成 1.1.1 CPU 1.1.2 主板 1.1.3 内存 1.1.4 硬盘 1.1.5 电源 1.1.6 显示器 1.1.7 键盘和鼠标 1.1.8 光驱 1.1.9 显卡 1.1.10 其他外部设备 1.2 软件组成 1.2.1 操作系统 Windows XP Windows 7 服务器操作系统 —— Windows Ser…

04-Nginx-conf配置文件基本了解

Nginx负载均衡&#xff0c;反向代理入门配置&#xff1a; nginx.conf整体结构 nginx入门基本配置 Nginx.conf配置文件详解&#xff08;upstream和location负载均衡和反向代理配置&#xff09;&#xff1a; #运行用户 user www-data; #启动进程,通常设置成和cpu的数量相等 wor…

基于边缘智能网关打造智慧体育场

运动健身是民众广泛存在的生活需求&#xff0c;体育场馆作为承载各种体育运动的基础设施&#xff0c;其运营管理效率、服务水平和智能化场景应用等都与用户体验紧密相关。 得益于物联网、边缘计算、AI智能等新技术的广泛应用&#xff0c;当前已有越来越多体育场馆通过部署基于…

数据结构与算法——Java实现稀疏数组和队列

目录 一、基本介绍 1.1 线性结构 1.2 非线性顺序结构 二、稀疏数组 2.1 基本介绍 2.1.1 应用场景 2.1.2 实现思路 2.2 代码实现 2.2.1 原始数组 2.2.2 原始数组转化为稀疏数组 2.2.3 稀疏数组转化为原始数组 三、队列的应用场景和介绍 3.1 数组模拟队列 3.1.1数组模拟队列的…

Find My资讯|Seinxon推出支持苹果 Find My 防丢卡

在美国&#xff0c;平均每个人每年丢失 3,000 件物品。而在 2021 年&#xff0c;Pixie 数据显示&#xff0c;丢失产品的更换成本超过 25 亿美元。每周超过两次&#xff0c;将近 1/4 的美国人丢失房门钥匙、钱包、宠物、电话、眼镜、耳机、遥控器、手提箱或孩子最喜欢的物品。 …

GIT系列(七)切换ssh连接,上传不再输入账号、密码

文章目录前言操作流程前言 使用HTTP连接方式时&#xff0c;上传代码总是需要登录&#xff0c;键盘都打坏了&#xff0c;切换SSH可以无需密码&#xff0c;直接上传。 操作流程 step 1 确保在git服务器已经部署本机公钥。 没有配置SSH的&#xff0c;戳这里 GIT系列&#xff08;…

k8s教程(18)-pod之DaemonSet(每个node上只调度一个pod)

文章目录01 引言02 DaemonSet2.1 应用场景2.2 举例2.3 注意事项03 文末01 引言 声明&#xff1a;本文为《Kubernetes权威指南&#xff1a;从Docker到Kubernetes实践全接触&#xff08;第5版&#xff09;》的读书笔记 DaemonSet是 Kubernetes1.2 版本新增的一种资源对象&#xf…

事件轮询机制 Event Loop、浏览器更新渲染时机、setTimeout VS setInterval

目录 1. 事件轮询机制&#xff08;Event Loop&#xff09;是什么 1.1 宏任务、微任务 1.2 Event Loop 循环过程 1.3 经典题目分析 1.3.1 第一轮事件循环 1.3.2 第二、三次事件循环 1.3.3 参考文章 2. async、await 在事件轮询中的执行时机 3. 浏览器更新渲染时机、Vue…

线上使用雪花算法生成id重复问题

项目中使用的是hutool工具类库提供的雪花算法生成id方式&#xff0c;版本使用的是5.3.1 <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.3.1</version></dependency>雪花算法生成id…

10分钟数仓实战之kettle整合Hadoop

1.写在前面 很多朋友在做数仓的ETL的动作的时候&#xff0c;还是喜欢比较易上手的kettle 前面章节有介绍过安装kettle&#xff0c;可以参考 ETL工具--安装kettle_老码试途的博客-CSDN博客_spoon.bat 安装 kettle在Windows系统中对数据的转换、表和文件的转换等&#xff0c;…

Blender 3D环境场景创建教程

Blender 3D环境场景创建教程 学习 Blender 3.2&#xff0c;探索几何节点并创建美妙的 3D 环境 课程英文名&#xff1a;Creating 3D Environments in Blender 2.81 by Rob Tuytel (2019) 此视频教程共8.0小时&#xff0c;中英双语字幕&#xff0c;画质清晰无水印&#xff0c;…

腾讯云从业者基础认证完整笔记

腾讯云从业者基础认证完整笔记 就考这些&#xff0c;干就完事儿了&#xff01;不要介意图多哟&#xff0c;ppt能更好的表达意思呀 一、云计算基础 1.1 数据中心 一般企业要么自建数据中心EDC&#xff0c;EDC分层如下&#xff1a; 要么租用或者托管也就是IDC如下&#xff…

ZYNQ之FPGA学习----EEPROM读写测试实验

1 EEPROM简介 EEPROM (Electrically Erasable Progammable Read Only Memory&#xff0c;E2PROM)即电可擦除可编程只读存储器&#xff0c;是一种常用的非易失性存储器(掉电数据不丢失)。ZYNQ开发板上使用的是AT24C64&#xff0c;通过IIC协议实现读写操作。IIC通信协议基础知识…

Oracle 11g---基于CentOS7

Oracle 11g安装教程 以下步骤基于网络配置完成&#xff0c;并且能连接xshell和xftp工具 文章目录Oracle 11g安装教程1.将oracle压缩包拷贝到安装机器&#xff0c;指定目录中2.安装依赖包3.验证依赖包4.创建oracle用户5.创建oradata目录,解压oracle安装6.修改系统配置参数7.创建…

2023年开始当年授权或转让的知识产权申报高新将不再认可。

前段时间&#xff0c;由国家科技部火炬中心组织全国高新技术企业管理机构召开会议&#xff0c;会议宣导要求加强企业知识产权管理&#xff0c;强调对当年授权或转让的专利&#xff0c;用来申报当年高新将不再认可。 、从多省市反馈的消息显示部分省市执行了该政策。虽然广东暂…

Java 2022圣诞树+2023元旦倒计时打包一起领走

2022最后一个月充满了期待&#xff0c;平安夜、圣诞节、元旦节&#xff1b;2023年也是一个早年&#xff0c;因此关于程序方面的浪漫&#xff0c;大家应该趁早准备。下面我将分享一个元旦的倒计时和圣诞树的绘制核心代码。大家可以依据自身的需求&#xff0c;稍微调整即可用。 …

振弦渗压计怎样安装?振弦式渗压计工作原理

振弦渗压计是一种长期测量混凝土或地基内的孔隙(渗透)水压力&#xff0c;并可同步测量埋设点温度。适用于大坝工程安全监测、尾矿库工程安全监测、各类公路、桥梁、隧洞安全监测、土工建筑物基坑安全监测等。    1、设备介绍 通过不断的生产工艺技术的积累&#xff0c;采用…