你怎么理解MyBatis-Plus selectOne查询一条记录方法的设计思想?

MyBatis-Plus 是 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。selectOne 是 MyBatis-Plus 中的一个方法,用于从数据库中查询并返回单个对象。

以下是MyBatis-Plus不同版本的源码,可以看出selectOne方法也是调用了selectList方法。

  • 3.5.3版本
    /**
     * 根据 entity 条件,查询一条记录
     * <p>查询一条记录,例如 qw.last("limit 1") 限制取一条记录, 注意:多条数据会报异常</p>
     *
     * @param queryWrapper 实体对象封装操作类(可以为 null)
     */
    default T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) {
        List<T> ts = this.selectList(queryWrapper);
        if (CollectionUtils.isNotEmpty(ts)) {
            if (ts.size() != 1) {
                throw ExceptionUtils.mpe("One record is expected, but the query result is multiple records");
            }
            return ts.get(0);
        }
        return null;
    }
  • 3.5.9版本(当前最新版本)
    /**
     * 根据 entity 条件,查询一条记录
     * <p>查询一条记录,例如 qw.last("limit 1") 限制取一条记录, 注意:多条数据会报异常</p>
     *
     * @param queryWrapper 实体对象封装操作类(可以为 null)
     */
    default T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) {
        return this.selectOne(queryWrapper, true);
    }

    /**
     * 根据 entity 条件,查询一条记录,现在会根据{@code throwEx}参数判断是否抛出异常,如果为false就直接返回一条数据
     * <p>查询一条记录,例如 qw.last("limit 1") 限制取一条记录, 注意:多条数据会报异常</p>
     *
     * @param queryWrapper 实体对象封装操作类(可以为 null)
     * @param throwEx      boolean 参数,为true如果存在多个结果直接抛出异常
     */
    default T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper, boolean throwEx) {
        List<T> list = this.selectList(queryWrapper);
        int size = list.size();
        if (size == 1) {
            return list.get(0);
        } else if (size > 1) {
            if (throwEx) {
                throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + size);
            }
            return list.get(0);
        }
        return null;
    }

更早版本的selectOne方法和selectOne方法的底层实现如出一辙,唯一差别就是返回类型一个是对象一个是集合!

接下来聊聊为什么会关注到这一块,因为目前大部分Java项目后台使用的框架都离不开Mybatis-Plus(确实很棒),有一天当我在部署一个项目的时候不小心将一个表的数据添加重复了,然后测试功能的时候报错:

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 2

当定位到代码位置,发现是执行selectOne方法时查询数据出现了多条的情况。然后当我搜索了一下整个项目,发现selectOne方法在代码中使用的非常频繁,我意识到这是一个问题!于是看了一下源码,发现selectOne并非如方法名“查询一条记录”这么简单,它的前置条件是数据只有满足条件的一条,而非只返回一条,如果不确定要查询的数据只有一条那就会抛出异常,所以selectOne使用时会存在风险。

下面我列举了一些使用 selectOne 方法的典型场景,我们思考一下selectOne方法是不是有点鸡肋,先看场景:

1、根据主键查询
当需要根据主键(Primary Key)查询一个实体对象时,selectOne 方法也适用。例如,在根据用户ID查询用户信息时,可以使用 selectOne 方法。

QueryWrapper<User> queryWrapper = new QueryWrapper<>();  
queryWrapper.eq("id", userId);  
User user = userMapper.selectOne(queryWrapper);

虽然 selectOne 可以结合条件构造器 QueryWrapper 或 LambdaQueryWrapper 使用来实现根据主键查询一个实体对象,但 selectById 是 MyBatis-Plus 提供的专门用于根据主键查询的方法,此处改为selectById 更优,所以根据主键查询100%我们应该选selectById。

User user = userMapper.selectOne(userId);

2、根据唯一条件查询
如果表中有唯一约束的字段(如邮箱、手机号等),并且需要根据这些字段查询单个记录时,可以使用 selectOne 方法。例如,根据邮箱查询用户信息:

QueryWrapper<User> queryWrapper = new QueryWrapper<>();  
queryWrapper.eq("email", email);  
User user = userMapper.selectOne(queryWrapper);

一旦表中没有维护好唯一约束的字段,当数据实际不唯一时就会查询到多条,这块就会抛出org.apache.ibatis.exceptions.TooManyResultsException 异常。

3、确保返回单条记录
在某些情况下,查询条件可能会返回多条记录,但业务逻辑上只需要其中的一条(例如,取最新的一条记录)。

//新版本写法
//selectOne方法的第二个参数为false时如果存在多个结果取第一个元素,不加或传true会抛出异常
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();  
queryWrapper.orderByDesc("create_time");  
Order latestOrder = orderMapper.selectOne(queryWrapper, false);


//老版本写法
//此时,虽然可以使用 selectList 方法并取第一个元素,但使用 selectOne 可以确保在返回多条记录时抛出异常,从而避免潜在的逻辑错误。
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();  
queryWrapper.orderByDesc("create_time").last("limit 1");  
Order latestOrder = orderMapper.selectOne(queryWrapper);

需要注意的是,如果查询条件确实可能返回多条记录,并且业务逻辑允许返回任意一条记录,则应该确保在调用 selectOne 之前已经通过其他方式(如数据库约束、业务逻辑等)确保了只会返回一条记录,否则可能会抛出 org.apache.ibatis.exceptions.TooManyResultsException 异常。

4、复杂查询中的单条记录获取
在复杂查询中,当使用联合查询、子查询或复杂的条件构造器时,如果预期结果只有一条记录,可以使用 selectOne 方法。例如,根据多个条件组合查询单个用户信息: 

LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();  
lambdaQueryWrapper.eq(User::getStatus, 1)  
                  .like(User::getName, nameKeyword)  
                  .orderByDesc(User::getCreateTime);  
User user = userMapper.selectOne(lambdaQueryWrapper);

总之,selectOne 方法在需要从数据库中查询并返回单个对象在BaseMapper开放的方法中很有必要,但是目前的源码需要确保根据查询条件查到的数据最多只有一条,出现多条时会抛出异常,要么主动避免。从源码看是调用selectList方法,然后get(0),想想看,当数据非常大的情况下,如果selectOne只需要返回第一条数据,性能会不会拉胯,很纳闷的事为何官方不直接在sql后加上“limit 1”来查询。有网友也说“相信这么成熟的框架应该不会犯这种低级错误,所以我觉得,要么报错,要么sql语句中加上limit 1才是合理的解释”。你这么理解?

猜你喜欢

转载自blog.csdn.net/u010709330/article/details/143366409