Mybatis-Plus

官方文档:MyBatis-Plus 🚀 为简化开发而生

学习视频在线文档

MyBatis 最佳搭档,只做增强不做改变,为简化开发、提高效率而生。

Relationship Between MyBatis and MyBatis-Plus

快速入门

入门案例

相较于单体项目,这需要两步就可以使用MybatisPlus

  1. 引入依赖

    MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配效果。因此我们可以用MybatisPlus的starter代替Mybatis的starter

    <!--M{ybatisPlus-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version>
    </dependency>
    
  2. 定义Mapper

    自定义的Mapper继承MybatisPlus提供的BaseMapper接口。注意这里的泛型要传入自己操作的实体类

    public interface UserMapper extends BaseMapper<User> {
        
    }
    

然后就可以直接用BaseMapper定义好的方法去实现增删改查了,不用写mapper.xml文件了。相当于继承已经写好的了,瞬间变成富二代,不用那么努力了。

userMapper.insert(user);
userMapper.selectById(5L);
userMapper.updateById(user); //没有传值的字段可以保持不变
userMapper.selectBatchIds(List.of(1L, 2L)); //idea也会有提示的,很方便

常见注解

mp是如何实现上述效果呢,我们可以继续学习。

  • MybatisPlus是如何获取实现CRUD的数据库表信息的?

    通过扫描实体类【通过泛型传入】,并基于反射获取实体类信息作为数据库表信息

    • 默认以类名驼峰转下划线作为表名
    • 默认把名为id的字段作为主键
    • 默认把变量名驼峰转下划线作为表的字段名
  • MybatisPlus的常用注解有哪些?

    表明字段名不符合约定和特殊字段时就要自定义了。通过注解去指明表的信息

    • @TableName:指定表名称及全局配置
    • @TableId:指定id字段及相关配置
    • @TableField:指定普通字段及相关配置
  • 在@TableName可以设置IdType,其常见类型有哪些?

    • AUTO:数据库自增长
    • ASSIGN_ID:通过set方法自行输入
    • INPUT【默认】:分配 ID,接口IdentifierGenerator的方法nextId来生成id,默认实现类为DefaultIdentifierGenerator雪花算法。

    也就是说,对应id字段尽管符合约定,但还是要指明类型的,否则会用雪花算法【生成一个long型的id】分配

  • 使用@TableField的常见场景是?

    • 成员变量名与数据库字段名不一致
    • 成员变量名以is开头,且是布尔值。否则在自动转换过程中会出错,会把is去掉作为变量名。
    • 成员变量名与数据库关键字冲突
    • 成员变量不是数据库字段

示例:

@Data
@TableName("tb_user")
public class User {

    //指定为主键,声明为自增。
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    //成员变量名与数据库字段名不一致
    @TableField("username")
    private String name;

    //成员变量名以is开头,是布尔值,否则误以为字段名是married了
    @TableField("is_married")
    private boolean isMarried;

    //成员变量名与数据库关键字冲突
    @TableField("`order`")
    private Integer order;

    //成员变量不是数据库字段。转换的时候忽略掉
    @TableField("exist = false")
    private String address;
}

常见配置

MyBatisPlus的配置项继承了MyBatis原生配置和一些自己特有的配置。例如:

mybatis-plus:
  type-aliases-package: com.ario.mp.domain.po # 别名扫描包
  mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,【默认值,符合默认值就不用配置】。这里可以支持多级目录的搜索。其实还是要写sql的,因为mp主要终于单表操作。
  configuration:
    map-underscore-to-camel-case: true # 是否开启下划线和驼峰的映射
    cache-enabled: false # 是否开启二级缓存
  global-config:
    db-config:
      id-type: assign_id # id为雪花算法生成
      update-strategy: not_null # 更新策略:只更新非空字段

其实到部分的配置和mybatis的配置相同,可以更好的上手。当然也有新的配置,比如:

  • global-config

别看着复杂,其实除了包的别名,都是有默认值的,一般不需要配置。

核心功能

条件构造器

我们上面都是用id实现的,我们很多情况需要用更复杂的条件筛选出想要的记录。mp提供了我们实现筛选的条件构造器。

MyBatisPlus支持各种复杂的where条件,可以满足日常开发的所有需求。

image-20251027175902824

下面是一些继承关系

image-20251027180006951

AbstractWrapper有很多方法能实现复杂的查询

image-20251027180044636

其中我们常用的就是UpdateWrapper和QueryWapper。具体可以看如下案例

  • 基于QueryWrapper的查询

    需求:

    • 查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段

      SELECT id,username,info,balance 
      FROM user 
      WHERE username LIKE ? AND balance >= ?
      
    • 更新用户名为jack的用户的余额为2000

      UPDATE user 
      SET balance = 2000 
      WHERE (username = "jack")
      

    代码示例:

    @Test
    void testQueryWrapper() {
        //1.利用条件构造器构建查询条件
        //支持链式编程
        QueryWrapper<User> queryWrapper = new QueryWrapper<User>()
                .select("id", "username", "info", "balance")
                .like("username", "o")
                .ge("balance", 1000);
        //2. 执行查询
        List<User> users = userMapper.selectList(queryWrapper);
    }
    
    @Test
    void TestUpdateByQueryWrapper() {
        //1. 要更新的数据
        User user = new User();
        user.setBalance(2000);
        //2. 更新的条件
        QueryWrapper<User> wrapper = new QueryWrapper<User>()
                .eq("username", "jack");
        //3. 执行更新,给数据和条件
        userMapper.update(user, wrapper);
    }
    
  • 基于UpdateWrapper的更新

    仅有QueryWrapper在某些情况下还是不行的。UpdateWrappe通常只有在set语句比较特殊才使用。案例如下:

    需求:更新id为1,2,4的用户的余额,扣200

    这需要在记录的现有值上修改,不能像那样写了

    UPDATE user 
    SET balance = balance - 200 
    WHERE id in (1, 2, 4)
    

    实现代码如下:

    @Test
    void testUpdateWrapper() {
        List<Long> ids = List.of(1L, 2L);
        UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>()
                .setSql("balance = balance - 200") //在这里手写sql即可
                .in("id", ids);
        userMapper.update(null, updateWrapper);
    }
    
  • LambdaQueryWrapper和LambdaUpdateWrapper

    观察上面的几个案例可以发现,我用的是硬编码,我们应该尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码。

    示例代码如下:

    @Test
    void testLambdaQueryWrapper() {
        LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>() //要用对应类的构造方法
                .select(User::getId, User::getUsername, User::getInfo, User::getBalance) //用get方法获取字段
                .like(User::getUsername, "o")
                .ge(User::getBalance, 1000);
        //2. 执行查询
        List<User> users = userMapper.selectList(lambdaQueryWrapper);
    }
    

自定义SQL

为什么还要自定义SQL呢?我们可以看到,mp确实简化了SQL语句,比如实现where的作用,上面的in("id", ids)大大的缩减了代码量,可以跟下面对比一下

SELECT *
FROM user
<if test="ids != null">
    WHERE id IN
    <foreach collection="ids" open="(" close=")" item="id" separator=",">
        #{id}
    </foreach>
</if>
LIMIT 10

但是,对于另外一部分的SQL:balance = balance - 200,则是直接写在了业务层,这违反了编码规范。

由此我们可以用自定义SQL解决这样的难题,我们把全自动改为半自动,及发挥出mp的优势,又保证编码符合规范

image-20251027203827391

示例代码如下:

  • 业务层代码

    @Test
    void testCustomSqlUpdate() {
        //1. 更新条件
        List<Long> ids = List.of(1L, 2L);
        int amount = 200;
        //2. 定义条件
        QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);//也可以使用lambda的方式
        //3. 调用自定义SQL方法
        userMapper.updateBalanceByIds(wrapper, amount);
    }
    
  • mapper层

    //当然,这里也可以用注释实现
    //必须要注解参数,这里Constants.WRAPPER是常量"ew",也可以直接写"ew"
    void updateBalanceByIds(@Param(Constants.WRAPPER) QueryWrapper<User> wrapper, @Param("amount") int amount);
    
  • mapper.xml层

    $用mp解析拼接

    <update id="updateBalanceByIds">
        UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}
    </update>
    

Service接口

基本用法

mp还提供了service代码,之后就不应写常用的service代码了,其本质还是继承别人的代码。下面是mp提供的接口。

image-20251027211217521

包括了常用的新增【单个、批量】、删除、更新和查询。

其中在批量删除可以区分一下

  • removeByIds用的是in
  • removeBatchByIds是用最简单的where id = ?的方式,然后jdbc批处理,数据量大时性能更好一点

其中lambda提供了链式编程的wrapper,不用new了。适合复杂条件的查询更新。

在实现中,我们继承就要继承全,只有接口继承是不可以的,还要继承实现类,因为要把接口中的类都实现

image-20251027211928310

代码示例:

  • 接口

    public interface IUserService extends IService<User> {
    }
    
  • 实现类

    //分别指出要用的mapper和实体类
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    }
    
  • 使用代码

    userService.save(user); //和之前代码无异
    

进入IUserService可以看到具体的实现,如果需要自定义SQL的方法,可以从中获取baseMapper实体类。

image-20251027213451550

简单业务开发

对于如下的简单业务接口可以直接调用IService进行开发。

编号 接口 请求方式 请求路径
1 新增用户 POST /users
2 删除用户 DELETE /users/
3 根据id查询用户 GET /users/
4 根据id批量查询 GET /users
  • @RequiredArgsConstructor

    在controller中注入service时,一般用构造函数注入,成员变量比较多是会先的很复杂,这是我们可以用Lombok提供的这个注解,实现按需注入。同时主要成员变量用final修饰,使其编程常量。

    import lombok.RequiredArgsConstructor;
    
    @RequiredArgsConstructor
    public class UserController {
        private final IUserService userService;
        ……
    }
    
  • hutool拷贝

    在用提供的接口时,接口要求的类和已有的类可能不一样,属性基本一致时,我们可以通过拷贝来进行转换,hutool工具就提供了很好的方法。

    单个拷贝:

    public void saveUser(@RequestBody UserFormDTO userFormDTO){
        // 1.转换DTO为PO
        User user = BeanUtil.copyProperties(userFormDTO, User.class);
        // 2.新增
        userService.save(user);
    }
    

    集合拷贝:

    public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids){
        // 1.查询用户
        List<User> users = userService.listByIds(ids);
        // 2.处理vo
        return BeanUtil.copyToList(users, UserVO.class);
    }
    

复杂业务开发

可以发现对于简单接口可以直接调用service方法。但对于复杂页面,需要编写自定义业务逻辑时,就需要自定义service方法,当业务百安写自定义sql语句是,依然要自定义方法,调用mapper,自定义sql语句

编号 接口 请求方式 请求路径
5 根据id扣减余额 PUT /users/{id}/deduction/ 用户id扣减金额

这个接口就是稍微复杂的,要扣款,首先要判断账户是否被冻结,余额是否充足。

示例代码如下:

Controller层

@PutMapping("{id}/deduction/{money}")
public void deductBalance(@PathVariable("id") Long id, @PathVariable("money") Integer money){
    userService.deductBalance(id, money);
}

Service层

接口

public interface IUserService extends IService<User> {
    void deductBalance(Long id, Integer money);
}

实现类【编程技巧:反向判断,减少嵌套】

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Override
    public void deductBalance(Long id, Integer money) {
        // 1.查询用户,这里就不用注入service了,因为本身就是service,直接调就行。或者调mapper,因为继承了IUserService,所以也不用注入mapper了,点进去调用对应的mapper即可。
        User user = getById(id);
        // 2.判断用户状态。编程技巧:反向判断,减少嵌套。
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常");
        }
        // 3.判断用户余额
        if (user.getBalance() < money) {
            throw new RuntimeException("用户余额不足");
        }
        // 4.扣减余额
        baseMapper.deductMoneyById(id, money);
    }
}

Mapper层

//这里就没必要传wrapper了
@Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
void deductMoneyById(@Param("id") Long id, @Param("money") Integer money);

Lambda方法

IService中还提供了Lambda功能来简化我们的复杂查询及更新功能。下面通过案例了解。

案例一

需求:实现一个根据复杂条件查询用户的接口,查询条件如下:

  • name:用户名关键字,可以为空
  • status:用户状态,可以为空
  • minBalance:最小余额,可以为空
  • maxBalance:最大余额,可以为空

其复杂的原因是,不确定根据哪些字段查,如果写动态SQL的话,字段较多时会写老大一堆。这里用Lambda简化。当然我们也可以用wrapper,但这里lambda不用new,用话更简洁一点。

代码示例:

@GetMapping("/list")
public List<UserVO> queryUsers(UserQuery query){
    // 1.组织条件
    String username = query.getName();
    Integer status = query.getStatus();
    Integer minBalance = query.getMinBalance();
    Integer maxBalance = query.getMaxBalance();
    // 2.查询用户
    List<User> users = userService.lambdaQuery()
            .like(username != null, User::getUsername, username) //若不为空,则执行,判断代码更简洁了。
            .eq(status != null, User::getStatus, status)
            .ge(minBalance != null, User::getBalance, minBalance)
            .le(maxBalance != null, User::getBalance, maxBalance)
            .list();//记得调用list,否则没执行,前面的作用时构造条件的。
    // 3.处理vo
    return BeanUtil.copyToList(users, UserVO.class);
}

注意:

  • 这里的代码都写的controller层了,实际开发中建议写道service中,否则显得臃肿。
  • 因为参数比较多,可以直接传对象。需要定义一个新的对象。这也是常规操作。对于传复杂数据可以用对象,常用命名如下:
    • dto封装前端查询条件
    • vo返回给前端查询结果
    • pojo对应数据库实体类

案例二

需求:改造根据id修改用户余额的接口,要求如下

  • 完成对用户状态校验
  • 完成对用户余额校验
  • 如果扣减后余额为0,则将用户status修改为冻结状态

示例代码如下:

@Override
@Transactional
public void deductBalance(Long id, Integer money) {
    // 1.查询用户
    User user = getById(id);
    // 2.校验用户状态
    if (user == null || user.getStatus() == 2) {
        throw new RuntimeException("用户状态异常!");
    }
    // 3.校验余额是否充足
    if (user.getBalance() < money) {
        throw new RuntimeException("用户余额不足!");
    }
    // 4.扣减余额 update tb_user set balance = balance - ?
    int remainBalance = user.getBalance() - money;
    lambdaUpdate()
            .set(User::getBalance, remainBalance) // 更新余额
            .set(remainBalance == 0, User::getStatus, 2) // 动态判断,是否更新status
            .eq(User::getId, id)
            .eq(User::getBalance, user.getBalance()) // 乐观锁,避免出错
            .update();//依旧要调用,否则不执行。
}

批量新增

批处理方案【例如10000条数据】:

  • 普通for循环逐条插入速度极差,不推荐。【因为每次网络请求只提交一条sql语句,每个记录逐条执行SQL】

    @Test
    void testSaveOneByOne() {
        long b = System.currentTimeMillis();
        for (int i = 1; i <= 100000; i++) {
            userService.save(buildUser(i));
        }
        long e = System.currentTimeMillis();
        System.out.println("耗时:" + (e - b));
    }
    
    private User buildUser(int i) {
        User user = new User();
        user.setUsername("user_" + i);
        user.setPassword("123");
        user.setPhone("" + (18688190000L + i));
        user.setBalance(2000);
        user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(user.getCreateTime());
        return user;
    }
    
  • MP的批量新增,基于预编译的批处理,性能不错。【因为每次网络请求提交多个sql语句,但每个记录依然是逐条执行SQL】

    @Test
    void testSaveBatch() {
        // 准备10万条数据
        List<User> list = new ArrayList<>(1000);
        long b = System.currentTimeMillis();
        for (int i = 1; i <= 100000; i++) {
            list.add(buildUser(i));
            // 每1000条批量插入一次。因为一次请求的数据量是有上限的。
            if (i % 1000 == 0) {
                userService.saveBatch(list);
                list.clear();
            }
        }
        long e = System.currentTimeMillis();
        System.out.println("耗时:" + (e - b));
    }
    
  • 配置jdbc参数,开rewriteBatchedStatements【默认是不生效的】,性能最好。【因为每次网络请求提交多个sql语句,但同时多条记录合并成一个SQL了】

    只在yaml文件中的连接数据库字段拼接如下字段接口,其他代码和“MP的批量新增”方式保存一直即可。

    &rewriteBatchedStatements=true
    

扩展功能

拓展拓展,没有也行

代码生成

虽然经过我们之前的工作已经大大简化了开发过程。但是还是可以有优化之处。对于实体类、controller、service、mapper可能只是因为表名不同,我们要重复建立类似的文件。有点费时间了,mp给我们提供了代码生成功能,会根据数据库表,生成这些基本的代码。

但是生成代码前,我们需要些一堆生成代码的代码,还是挺麻烦的,我们可以用插件。

image-20251028183748389

我们可以用官方提供的Mybatis X 插件,也可以用其他插件。在这了可以用MyBatisPlus插件。

image-20251028183924012

安装完成后,需要我们做一些简单的配置。

  1. 在Other->Config Database,填写数据库信息。【dbUrl记得加数据库名】

    image-20251028184101047

    image-20251028184344687

  2. 填写表生成信息,生成代码

    • 可以多选数据表,但是别选错了,会覆盖之前的代码。
    • module是用于区分子模块的
    • package填controller、service这里包的上一级。
    • TablePrefix,如果不想要数据库表名的前缀带到类中,可以填这个。例如tb_user表,填tb_,能生产User类。

    image-20251028184432960

静态工具

有的时候Service之间也会相互调用【可以要用到多个查询语句,当然也可以用连接查询】,为了避免出现循环依赖问题【当然,也可以直接注入mapper,但是mapper不如service功能强大】,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能。

静态工具和IService的区别就是,要告诉它实体类字节码,因为他是静态的,不能像IService那样拿到实体类上的泛型。

案例:改造根据id用户查询的接口,查询用户的同时返回用户收货地址列表【涉及到两个表】

核心代码:【UserVO需添加address字段】

@Override
public UserVO queryUserAndAddressById(Long userId) {
    // 1.查询用户
    User user = getById(userId);
    if (user == null) {
        return null;
    }
    // 2.查询收货地址
    List<Address> addresses = Db.lambdaQuery(Address.class)
            .eq(Address::getUserId, userId)
            .list();
    // 3.处理vo
    UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
    userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
    return userVO;
}

补充案例:

根据id批量查询用户,并查询出用户对应的所有地址

这个实现建议先分别查出用户和地址,然后通过stream流的方式根据用户id进行分组【用过Collectors.groupingBy的方法】。一起转换成要返回的userVO。否则,如果嵌套查询的话效率会很低。

逻辑删除

如果把购物车里面的订单删掉,商家的订单肯定式不能变的。

实现逻辑删除,可以增加一个状态字段即可。但这与传统删除不一样了,可以说式更新。那之前mp提供的接口还能用吗,mp提供了方法,轻松实现。

我们只需要在配置文件中开启逻辑删除即可。之后调用之前的接口就是逻辑删除了【用法完全一样】。【当然表中要有对应字段】

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

可以在日志中看到,删除对应的操作实际上被转化为了更新操作。

但也有缺点:

  • 会导致数据库表垃圾数据越来越多,从而影响查询效率
  • SQL中全都需要对逻辑删除字段做判断,影响查询效率

因此,不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。

枚举处理器

数据库和Java枚举转换

在对对象状态做判断时【比如冻结/正常,未通过/通过】,我们数据库采用的是int类型,对应的PO也是Integer。因此业务操作时必须手动把枚举Integer转换,非常麻烦。

因此,MybatisPlus提供了一个处理枚举的类型转换器,可以把枚举类型与数据库类型自动转换

  1. 首先我们要定义一个枚举

    import com.baomidou.mybatisplus.annotation.EnumValue;
    import lombok.Getter;
    
    @Getter
    public enum UserStatus {
        NORMAL(1, "正常"),
        FREEZE(2, "冻结")
        ;
        @EnumValue //告诉MybatisPlus,枚举中的哪个字段的值作为数据库值
        private final int value;
        private final String desc;
    
        UserStatus(int value, String desc) {
            this.value = value;
            this.desc = desc;
        }
    }
    

    同时,实体类和返回类等涉及到该字段的也要变,用枚举类修改变量

    private UserStatus status;
    
  2. 配置,开启功能。用mp进行数据库和Java枚举转换

    mybatis-plus:
      configuration:
        default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
    
  3. 然后没有就可以在代码直接使用啦。但这里要注意,这里查到的默认是枚举项名NORMAL/FROZEN。如果想指定,也可以通过注释指定【告诉SpringMVC要传什么】,如下

    @JsonValue
    private final String desc;
    

JSON类型处理器

如果数据库的user表中有一个info字段,是JSON类型的。但是Java中没有json类型。所以需要转换也行,mp给我提供了类型处理器。分为两步。

  1. 定义实体

    import lombok.Data;
    
    @Data
    public class UserInfo {
        private Integer age;
        private String intro;
        private String gender;
    }
    
  2. 使用类型处理器【mp没有提供专门的配置,需要用注释声明】,同时,因为出现了类的嵌套,需要在User类上添加一个注解,声明自动映射。这样就免的我们定义复杂的resultMap了。

    @TableName(value = "user", autoResultMap = true)
    public class User {
        ……
        @TableField(typeHandler = JacksonTypeHandler.class)
        private UserInfo info;
        ……
    }
    

插件功能

MyBatisPlus基于MyBatis的Interceptor实现了一个基础拦截器,并在内部保存了MyBatisPlus的内置拦截器的集合:

image-20251028214400759

image-20251028214428438

MyBatisPlus提供的内置拦截器有下面这些:

序号 拦截器 描述
1 TenantLineInnerInterceptor 多租户插件
2 DynamicTableNameInnerInterceptor 动态表名插件
3 PaginationInnerInterceptor 分页插件
4 OptimisticLockerInnerInterceptor 乐观锁插件
5 IllegalSQLInnerInterceptor SQL性能规范插件,检测并拦截垃圾SQL
6 BlockAttackInnerInterceptor 防止全表更新和删除的插件

具体细节可以在官网了解学习。

posted @ 2025-10-27 21:40  韩熙隐ario  阅读(6)  评论(0)    收藏  举报