单表的CRUD功能代码重复度很高,也没有什么难度。而这部分代码量往往比较大,开发起来比较费时。因此,目前企业中都会使用一些组件来简化或省略单表的CRUD开发工作。目前在国内使用较多的一个组件就是MybatisPlus.
 官方网站如下:
 简介 | MyBatis-Plus
 当然,MybatisPlus不仅仅可以简化单表操作,而且还对Mybatis的功能有很多的增强。可以让我们的开发更加的简单,高效。
1.CRUD快速入门
一、Spring Boot整合Mybatis Plus
第一步:通过maven坐标引入依赖
<!-- mybatis -->
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.5.3.1</version>
</dependency>
<!-- mysql -->
<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
  <scope>runtime</scope>
</dependency>
<!-- lombok -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <optional>true</optional>
</dependency>
第二步:application配置数据源及日志输出级别
# 配置数据源
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
# 配置日志
logging:
  level:
    com.itheima: debug
  pattern:
    dateformat: HH:mm:ss
第三步:配置Mybatis的Mapper类文件的包扫描路径
@MapperScan("com.itheima.mp.mapper")
@SpringBootApplication
public class MpDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(MpDemoApplication.class, args);
    }
}
二、编码构建实体和Mapper
编写实体类User.java
@Data
@TableName(value = "user", autoResultMap = true)
public class User {
    /**
     * 用户id
     */
    @TableId
    private Long id;
    /**
     * 用户名
     */
    @TableField("`username`")
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 注册手机号
     */
    private String phone;
    /**
     * 详细信息
     */
    @TableField(typeHandler = JacksonTypeHandler.class)
    private UserInfo info;
    /**
     * 使用状态(1正常 2冻结)
     */
    private UserStatus status;
    /**
     * 账户余额
     */
    private Integer balance;
    /**
     * 创建时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    private LocalDateTime createTime;
    /**
     * 更新时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    private LocalDateTime updateTime;
}
编写Mapper类UserMapper.java
@Mapper
@Repository
public interface UserMapper extends BaseMapper<User> {
}
2.常见注解
在刚刚的入门案例中,我们仅仅引入了依赖,继承了BaseMapper就能使用MybatisPlus,非常简单。但是问题来了:
 MybatisPlus如何知道我们要查询的是哪张表?表中有哪些字段呢?
大家回忆一下,UserMapper在继承BaseMapper的时候指定了一个泛型:
 
 泛型中的User就是与数据库对应的PO.
MybatisPlus就是根据PO实体的信息来推断出表的信息,从而生成SQL的。默认情况下:
- MybatisPlus会把PO实体的类名驼峰转下划线作为表名
- MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
- MybatisPlus会把名为id的字段作为主键
但很多情况下,默认的实现与实际场景不符,因此MybatisPlus提供了一些注解便于我们声明表信息。
2.1.@TableName
说明:
- 描述:表名注解,标识实体类对应的表
- 使用位置:实体类
示例:
@TableName("user")
public class User {
    private Long id;
    private String name;
}
TableName注解除了指定表名以外,还可以指定很多其它属性:
| 属性 | 类型 | 必须指定 | 默认值 | 描述 | 
|---|---|---|---|---|
| value | String | 否 | “” | 表名 | 
| schema | String | 否 | “” | schema | 
| keepGlobalPrefix | boolean | 否 | false | 是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时) | 
| resultMap | String | 否 | “” | xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定) | 
| autoResultMap | boolean | 否 | false | 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入) | 
| excludeProperty | String[] | 否 | {} | 需要排除的属性名 @since 3.3.1 | 
2.2@TableId
说明:
- 描述:主键注解,标识实体类中的主键字段
- 使用位置:实体类的主键字段
示例:
@TableName("user")
public class User {
    @TableId
    private Long id;
    private String name;
}
TableId注解支持两个属性:
| 属性 | 类型 | 必须指定 | 默认值 | 描述 | 
|---|---|---|---|---|
| value | String | 否 | “” | 表名 | 
| type | Enum | 否 | IdType.NONE | 指定主键类型 | 
IdType支持的类型有:
| 值 | 描述 | 
|---|---|
| AUTO | 数据库 ID 自增 | 
| NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) | 
| INPUT | insert 前自行 set 主键值 | 
| ASSIGN_ID | 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法) | 
| ASSIGN_UUID | 分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法) | 
| 分布式全局唯一 ID 长整型类型(please use ASSIGN_ID) | |
| 32 位 UUID 字符串(please use ASSIGN_UUID) | |
| 分布式全局唯一 ID 字符串类型(please use ASSIGN_ID) | 
这里比较常见的有三种:
- AUTO:利用数据库的id自增长
- INPUT:手动生成id
- ASSIGN_ID:雪花算法生成- Long类型的全局唯一id,这是默认的ID策略
2.3.@TableField
说明:
描述:普通字段注解
示例:
@TableName("user")
public class User {
    @TableId
    private Long id;
    private String name;
    private Integer age;
    @TableField("isMarried")
    private Boolean isMarried;
    @TableField("`concat`")
    private String concat;
}
一般情况下我们并不需要给字段添加@TableField注解,一些特殊情况除外:
- 成员变量名与数据库字段名不一致
- 成员变量是以isXXX命名,按照JavaBean的规范,MybatisPlus识别字段时会把is去除,这就导致与数据库不符。
- 成员变量名与数据库一致,但是与数据库的关键字冲突。使用@TableField注解给字段名添加````转义
支持的其它属性如下:
| 属性 | 类型 | 必填 | 默认值 | 描述 | 
|---|---|---|---|---|
| value | String | 否 | “” | 数据库字段名 | 
| exist | boolean | 否 | true | 是否为数据库表字段 | 
| condition | String | 否 | “” | 字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s},参考(opens new window) | 
| update | String | 否 | “” | 字段 update set 部分注入,例如:当在version字段上注解update=“%s+1” 表示更新时会 set version=version+1 (该属性优先级高于 el 属性) | 
| insertStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_NULL | 
| insert into table_a(column) values (#{columnProperty}) | ||||
| updateStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:IGNORED | 
| update table_a set column=#{columnProperty} | ||||
| whereStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_EMPTY | 
| where column=#{columnProperty} | ||||
| fill | Enum | 否 | FieldFill.DEFAULT | 字段自动填充策略 | 
| select | boolean | 否 | true | 是否进行 select 查询 | 
| keepGlobalFormat | boolean | 否 | false | 是否保持使用全局的 format 进行处理 | 
| jdbcType | JdbcType | 否 | JdbcType.UNDEFINED | JDBC 类型 (该默认值不代表会按照该值生效) | 
| typeHandler | TypeHander | 否 | ||
| 类型处理器 (该默认值不代表会按照该值生效) | ||||
| numericScale | String | 否 | “” | 指定小数点后保留的位数 | 
3.常见配置
MybatisPlus也支持基于yaml文件的自定义配置,详见官方文档:
 使用配置 | MyBatis-Plus
大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:
- 实体类的别名扫描包
- 全局id类型
mybatis-plus:
  type-aliases-package: com.itheima.mp.domain.po
  global-config:
    db-config:
      id-type: auto # 全局id类型为自增长
需要注意的是,MyBatisPlus也支持手写SQL的,而mapper文件的读取地址可以自己配置:
mybatis-plus:
  mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,当前这个是默认值。
可以看到默认值是classpath*:/mapper/**/*.xml,也就是说我们只要把mapper.xml文件放置这个目录下就一定会被加载。
例如,我们新建一个UserMapper.xml文件:
 
 然后在其中定义一个方法:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mp.mapper.UserMapper">
    <select id="queryById" resultType="User">
        SELECT * FROM user WHERE id = #{id}
    </select>
</mapper>
然后在测试类UserMapperTest中测试该方法:
@Test
void testQuery() {
    User user = userMapper.queryById(1L);
    System.out.println("user = " + user);
}
4.核心功能
刚才的案例中都是以id为条件的简单CRUD,一些复杂条件的SQL语句就要用到一些更高级的功能了。
4.1.条件构造器
除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id作为where条件以外,还支持更加复杂的where条件。
 
 参数中的Wrapper就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
 
Wrapper的子类AbstractWrapper提供了where中包含的所有条件构造方法:
 
 而QueryWrapper在AbstractWrapper的基础上拓展了一个select方法,允许指定查询字段:
 
 而UpdateWrapper在AbstractWrapper的基础上拓展了一个set方法,允许指定SQL中的SET部分:
 
接下来,我们就来看看如何利用Wrapper实现复杂查询。
4.1.1.QueryWrapper
无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。接下来看一些例子:
 查询:查询出名字中带o的,存款大于等于1000元的人。代码如下:
@Test
void testQueryWrapper() {
    // 1.构建查询条件 where name like "%o%" AND balance >= 1000
    QueryWrapper<User> wrapper = new QueryWrapper<User>()
            .select("id", "username", "info", "balance")
            .like("username", "o")
            .ge("balance", 1000);
    // 2.查询数据
    List<User> users = userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}
更新:更新用户名为jack的用户的余额为2000,代码如下:
@Test
void testUpdateByQueryWrapper() {
    // 1.构建查询条件 where name = "Jack"
    QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack");
    // 2.更新数据,user中非null字段都会作为set语句
    User user = new User();
    user.setBalance(2000);
    userMapper.update(user, wrapper);
}
4.1.2.UpdateWrapper
基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现。
 例如:更新id为1,2,4的用户的余额,扣200,对于的SQL应该是:
UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)
SET的赋值结果是基于字段现有值的,这个时候就要利用UpdateWrapper中的setSql功能了:
@Test
void testUpdateWrapper() {
    List<Long> ids = List.of(1L, 2L, 4L);
    // 1.生成SQL
    UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
            .setSql("balance = balance - 200") // SET balance = balance - 200
            .in("id", ids); // WHERE id in (1, 2, 4)
	// 2.更新,注意第一个参数可以给null,也就是不填更新字段和数据,
    // 而是基于UpdateWrapper中的setSQL来更新
    userMapper.update(null, wrapper);
}
4.1.3.LambdaQueryWrapper
无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,会出现字符串魔法值。这在编程规范中显然是不推荐的。
 那怎么样才能不写字段名,又能知道字段名呢?
其中一种办法是基于变量的gettter方法结合反射技术。因此我们只要将条件对应的字段的getter方法传递给MybatisPlus,它就能计算出对应的变量名了。而传递方法可以使用JDK8中的方法引用和Lambda表达式。
 因此MybatisPlus又提供了一套基于Lambda的Wrapper,包含两个:
- LambdaQueryWrapper
- LambdaUpdateWrapper
分别对应QueryWrapper和UpdateWrapper
其使用方式如下:
@Test
void testLambdaQueryWrapper() {
    // 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000
    QueryWrapper<User> wrapper = new QueryWrapper<>();
    wrapper.lambda()
            .select(User::getId, User::getUsername, User::getInfo, User::getBalance)
            .like(User::getUsername, "o")
            .ge(User::getBalance, 1000);
    // 2.查询
    List<User> users = userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}
更多构造器使用方法总结
 
4.2.自定义SQL
在演示UpdateWrapper的案例中,我们在代码中编写了更新的SQL语句:
 
 这种写法在某些企业也是不允许的,因为SQL语句最好都维护在持久层,而不是业务层。就当前案例来说,由于条件是in语句,只能将SQL写在Mapper.xml文件,利用foreach来生成动态SQL。
 这实在是太麻烦了。假如查询条件更复杂,动态SQL的编写也会更加复杂。
所以,MybatisPlus提供了自定义SQL功能,可以让我们利用Wrapper生成查询条件,再结合Mapper.xml编写SQL
4.2.1.基本用法
以当前案例来说,我们可以这样写:
@Test
void testCustomWrapper() {
    // 1.准备自定义查询条件
    List<Long> ids = List.of(1L, 2L, 4L);
    QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);
    // 2.调用mapper的自定义方法,直接传递Wrapper
    userMapper.deductBalanceByIds(200, wrapper);
}
然后在UserMapper中自定义SQL:
public interface UserMapper extends BaseMapper<User> {
    @Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")
    void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper);
}
这样就省去了编写复杂查询条件的烦恼了。
4.2.2.多表关联
理论上来将MyBatisPlus是不支持多表查询的,不过我们可以利用Wrapper中自定义条件结合自定义SQL来实现多表查询的效果。
 例如,我们要查询出所有收货地址在北京的并且用户id在1、2、4之中的用户
 要是自己基于mybatis实现SQL,大概是这样的:
  <select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
      SELECT *
      FROM user u
      INNER JOIN address a ON u.id = a.user_id
      WHERE u.id
      <foreach collection="ids" separator="," item="id" open="IN (" close=")">
          #{id}
      </foreach>
      AND a.city = #{city}
  </select>
可以看出其中最复杂的就是WHERE条件的编写,如果业务复杂一些,这里的SQL会更变态。但是基于自定义SQL结合Wrapper的玩法,我们就可以利用Wrapper来构建查询条件,然后手写SELECT及FROM部分,实现多表查询。
 查询条件这样来构建:
@Test
void testCustomJoinWrapper() {
    // 1.准备自定义查询条件
    QueryWrapper<User> wrapper = new QueryWrapper<User>()
            .in("u.id", List.of(1L, 2L, 4L))
            .eq("a.city", "北京");
    // 2.调用mapper的自定义方法
    List<User> users = userMapper.queryUserByWrapper(wrapper);
    users.forEach(System.out::println);
}
然后在UserMapper中自定义方法:
@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")
List<User> queryUserByWrapper(@Param("ew")QueryWrapper<User> wrapper);
当然,也可以在UserMapper.xml中写SQL:
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
    SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}
</select>
4.3.Service接口
MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。
 通用接口为IService,默认实现为ServiceImpl,其中封装的方法可以分为以下几类:
- save:新增
- remove:删除
- update:更新
- get:查询单个结果
- list:查询集合结果
- count:计数
- page:分页查询
4.3.1.CRUD
我们先俩看下基本的CRUD接口。
 新增:
 
- save是新增单个元素
- saveBatch是批量新增
- saveOrUpdate是根据id判断,如果数据存在就更新,不存在则新增
- saveOrUpdateBatch是批量的新增或修改
删除:
 
- removeById:根据id删除
- removeByIds:根据id批量删除
- removeByMap:根据Map中的键值对为条件删除
- remove(Wrapper<T>):根据Wrapper条件删除
- ~~removeBatchByIds~~:暂不支持
修改:
 
- updateById:根据id修改
- update(Wrapper<T>):根据- UpdateWrapper修改,- Wrapper中包含- set和- where部分
- update(T,Wrapper<T>):按照- T内的数据修改与- Wrapper匹配到的数据
- updateBatchById:根据id批量修改
Get:
 
- getById:根据id查询1条数据
- getOne(Wrapper<T>):根据- Wrapper查询1条数据
- getBaseMapper:获取- Service内的- BaseMapper实现,某些时候需要直接调用- Mapper内的自定义- SQL时可以用这个方法获取到- Mapper
List:
 
- listByIds:根据id批量查询
- list(Wrapper<T>):根据Wrapper条件查询多条数据
- list():查询所有
Count:
 
- count():统计所有数量
- count(Wrapper<T>):统计符合- Wrapper条件的数据数量
getBaseMapper:
 当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法:
 
4.3.2.基本用法
由于Service中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService,而是自定义Service接口,然后继承IService以拓展方法。同时,让自定义的Service实现类继承ServiceImpl,这样就不用自己实现IService中的接口了。
首先,定义UserService,继承IService:
package com.itheima.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;
public interface UserService extends IService<User> {
    // 拓展自定义方法
}
然后,编写UserServiceImpl类,继承ServiceImpl,实现UserService:
package com.itheima.mp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.po.service.UserService;
import com.itheima.mp.mapper.UserMapper;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
项目结构如下:
 
最后,编写一个测试类,测试一下:
package com.itheima.mp.service;
import com.itheima.mp.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserServiceTest {
    @Autowired
    UserService userService;
    @Test
    void testService() {
        List<User> list = userService.list();
        list.forEach(System.out::println);
    }
}
4.3.3.Lambda
Service中对LambdaQueryWrapper和LambdaUpdateWrapper的用法进一步做了简化。我们无需自己通过new的方式来创建Wrapper,而是直接调用lambdaQuery和lambdaUpdate方法:
基于Lambda查询:
@Test
void testLambdaQuery() {
    // 1.查询1个
    User rose = userService.lambdaQuery()
            .eq(User::getUsername, "Rose")
            .one(); // .one()查询1个
    System.out.println("rose = " + rose);
    // 2.查询多个
    List<User> users = userService.lambdaQuery()
            .like(User::getUsername, "o")
            .list(); // .list()查询集合
    users.forEach(System.out::println);
    // 3.count统计
    Long count = userService.lambdaQuery()
            .like(User::getUsername, "o")
            .count(); // .count()则计数
    System.out.println("count = " + count);
}
可以发现lambdaQuery方法中除了可以构建条件,而且根据链式编程的最后一个方法来判断最终的返回结果,可选的方法有:
- .one():最多1个结果
- .list():返回集合结果
- .count():返回计数结果
lambdaQuery还支持动态条件查询。比如下面这个需求:
定义一个方法,接收参数为username、status、minBalance、maxBalance,参数可以为空。
- 如果username参数不为空,则采用模糊查询;
- 如果status参数不为空,则采用精确匹配;
- 如果minBalance参数不为空,则余额必须大于minBalance
- 如果maxBalance参数不为空,则余额必须小于maxBalance
这个需求就是典型的动态查询,在业务开发中经常碰到,实现如下:
@Test
void testQueryUser() {
    List<User> users = queryUser("o", 1, null, null);
    users.forEach(System.out::println);
}
public List<User> queryUser(String username, Integer status, Integer minBalance, Integer maxBalance) {
    return 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();
}
基于Lambda更新:
@Test
void testLambdaUpdate() {
    userService.lambdaUpdate()
            .set(User::getBalance, 800) // set balance = 800
            .eq(User::getUsername, "Jack") // where username = "Jack"
            .update(); // 执行Update
}
lambdaUpdate()方法后基于链式编程,可以添加set条件和where条件。但最后一定要跟上update(),否则语句不会执行。
lambdaUpdate()同样支持动态条件,例如下面的需求:
基于IService中的lambdaUpdate()方法实现一个更新方法,满足下列需求:
- 参数为balance、id、username
- id或username至少一个不为空,根据id或username精确匹配用户
- 将匹配到的用户余额修改为balance
- 如果balance为0,则将用户status修改为冻结状态
实现如下:
@Test
void testUpdateBalance() {
    updateBalance(0L, 1L, null);
}
public void updateBalance(Long balance, Long id, String username){
    userService.lambdaUpdate()
            .set(User::getBalance, balance)
            .set(balance == 0, User::getStatus, 2)
            .eq(id != null, User::getId, id)
            .eq(username != null, User::getId, username)
            .update();
}
4.4.静态工具
有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能:
 
示例:
@Test
void testDbGet() {
    User user = Db.getById(1L, User.class);
    System.out.println(user);
}
@Test
void testDbList() {
    // 利用Db实现复杂条件查询
    List<User> list = Db.lambdaQuery(User.class)
            .like(User::getUsername, "o")
            .ge(User::getBalance, 1000)
            .list();
    list.forEach(System.out::println);
}
@Test
void testDbUpdate() {
    Db.lambdaUpdate(User.class)
            .set(User::getBalance, 2000)
            .eq(User::getUsername, "Rose");
}



















