MyBatis-Plus

94 阅读9分钟

通用pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jdbc</artifactId>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
        <version>3.5.0</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.13</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.2</version>
    </dependency>

    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

前提回顾

resultMap比resultType更灵活。

<resultMap id = "xxxMap" type="brand">
    // 主键列使用 id标签
    <id column = "表列名" property="实体类属性名"/>

    // 普通列使用 result标签
    <result column = "数据库表列名" property="属性名"/>
    ...
</resultMap>

使用:
<select id = "selectAll" resultMap="xxxMap">
    // * 可以自动映射到xxxMap中的数据
    select * from tb_brand
</select>

BaseMapper功能

@SpringBootTest
class Atguigu01MybatisplusApplicationTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    void contextLoads() {
        List<User> list = userMapper.selectList(null);
        list.forEach(System.out::println);
    }

//    新增功能
    @Test
    public void testInsert(){
//        insert into user(id,name,age,email) values (?,?,?,?)

        User user = new User();
        user.setAge(18);
        user.setEmail("zs@qq.com");
        user.setName("张三");

        // 返回新增的数据量,默认mybatis-plus生成的主键id用的是雪花算法
        int result = userMapper.insert(user);
        System.out.println(result);
    }

//    删除功能:返回删除的数量
    @Test
    public void testDelete(){
        // 如果不加一个L,数值就会超过int型
//        int result = userMapper.deleteById(1584762459243786242L);// 根据id来删除

        /* 根据map集合中设置的条件来删除
        Map<String,Object> map = new HashMap<>();
        map.put("name","张三");
        map.put("age",18);
        int result = userMapper.deleteByMap(map);
        */

        /* 通过多个id,批量删除表数据,传入的参数是一个集合
        将数组转化成List集合的方法
        List<Long> list = Arrays.asList(1L, 2L);
        int result = userMapper.deleteBatchIds(list);
        */

//        System.out.println(result);
    }

//    修改功能:
    @Test
    public void testUpdate(){
        User user = new User();
        user.setId(3L);
        user.setName("Xhan");
        user.setEmail("666@qq.com");
        int result = userMapper.updateById(user);
        System.out.println(result);
    }

//    查询功能:
    @Test
    public void testQuery(){
//        User user = userMapper.selectById(3L);

        /*
        List<Long> list1 = Arrays.asList(3L, 4L);
        List<User> list = userMapper.selectBatchIds(list1);
        list.forEach(System.out::println);
        */

        /*
        Map<String,Object> map = new HashMap<>();
        map.put("name","Xhan");
        map.put("age",28);

        List<User> list = userMapper.selectByMap(map);
         */

//        删除和修改也有类似这种方法,可以不写参数,默认就是所有,如果是所有,除了查询,修改和删除不推荐默认所有
        List<User> list = userMapper.selectList(null);
        list.forEach(System.out::println);
    }
}

自定义sql

  1. 先在MyBatisPlus的Mapper.xml中定义:
@Repository
public interface UserMapper extends BaseMapper<User> {
//    自定义数据库语句,需要自定义Mapper,并且要存在mapper文件夹中
    Map<String,Object> selectMapById(Long id);
}
  1. 然后需要在resources文件夹中再次创建一个mapper文件夹,里面存储XXXMapper.xml,该文件中的自定义sql语句和MyBatis的写法是一致的。
<?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.example.atguigu_01_mybatisplus.mapper.UserMapper">
    <!-- Map<String,Object> selectMapById(Long id); -->
    <select id="selectMapById" resultType="map">
        select id,name,age,email from user where id = #{id}
    </select>
</mapper>
  1. 使用:
@Test
public void testMySql(){
    Map<String, Object> map = userMapper.selectMapById(3L);
    System.out.println(map);
}

Service接口中的CRUD

CRUD 接口 | MyBatis-Plus (baomidou.com)

@SpringBootTest
public class MyBatisPlusServiceTest {
    @Autowired
    private UserService userService;

//    查询总数据数方法:SELECT COUNT( * ) FROM user
    @Test
    void testGetCount(){
        long count = userService.count();
        System.out.println("总记录数:"+count);
    }

//    批量添加:本质上是单个sql语句,循环添加
//    INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
    @Test
    void testInsertMore(){
        List<User> list = new ArrayList<>();
        for(int i=0;i<10;i++){
            User user = new User();
            user.setName("Xhan"+i);
            user.setEmail("123@qq.com");
            user.setAge(i+1);
            list.add(user);
        }
        boolean b = userService.saveBatch(list); // 返回批量是否成功
        System.out.println(b);
    }
}

注解@TableName

我们在项目中并没有指定对哪张表操作,MyBatisPlus是通过继承BaseMapper时,指定了类型为<User>也就是实体类类型,才匹配到数据中的User表,一旦表名发生改变,那么就不会匹配上了。

第一种方法:TableName("user")指定对应的表名

如果表名具有相同的前缀,比如果t_,我们可以在yml中进行设置:

global-config:
  db-config:
    table-prefix: t_

注解@TableId

MyBatisPlus默认会将id设置为主键,并通过雪花算法生成唯一的主键值。如果我们id不是主键,或者说主键名不叫id,那么就会存在问题。

  • 在实体类中,对实体类中匹配字段主键的属性,设置@TableId指定主键(字段和实体类的名字要相同,只不过不叫id)。

  • 那如果字段和实体类的名字不同呢?比如字段是uid,而实体类中是id:通过@TableId(value="字段名")来指定主键的字段

@TableId(value = "uid")
private Long id;
  • 不想使用MyBatisPlus中的主键雪花算法,想要自动递增,首先要把主键字段设置成递增,然后设置@TableId(type=IdType.AUTO),因为默认type是雪花算法类型(IdType.ASSIGN_ID)。
@TableId(value="uid" type=IdType.AUTO)
private Long id;
  • 如果在修改操作时,我们指定id的值,那么id的值还会是雪花算法吗?不会的。和主键自增一样,手动优先。

我们也可以全局设置自增的策略:

global-config:
  db-config:
    id-type:auto;

雪花算法

image.png

image.png

  • 垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。

    • 例如,前面示意图中的 nickname 和 description 字段,假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外 一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。
  • 水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。

    • 对于一些比较复杂的表,可能超过 1000万就要分表了;而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表。但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性 能瓶颈或者隐患。

水平分表相比垂直分表,会引入更多的复杂性,例如要求全局唯一的数据id该如何处理。

image.png

image.png

image.png

注解@TableField

image.png

MyBatisPlus可以将字段中下划线的名字,替换成驼峰,但是如果名字整个就不同,就需要我们使用注解来解决。

@TableFiled用来解决除了主键外的字段名和实体类属性不匹配的问题。

@TableField(user_name)
private String name;

注解@TableLogic

image.png

  1. 在表中添加一个字段:isDeleted,int型,默认是0。
  2. 然后在实体类中定义isDeleted。
  3. 对这个属性加上注解:@TableLogic表示逻辑删除字段。

此时删除操作就会变成修改操作:is_deleted默认是0,表示未删除状态,如果为1,表示已删除状态(逻辑删除)。

测试删除功能,真正执行的是修改is_deleted的值,但是查询的话,还是查不到的,因为被逻辑删除了,只能查询到id_deleted=0未删除的数据。但是打开数据表,还是存在的,只不过is_deleted=1

UPDATE t_user SET is_deleted=1 WHERE id=? AND is_deleted=0  

测试查询功能,被逻辑删除的数据默认不会被查询

SELECT id,username AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0

条件构造器

在BaseMapper中,老是能看到一个参数:Wrapper,他实质上是一个条件构造器,用来封装条件。

image.png

组装各种条件

默认就是and条件

@SpringBootTest
public class MyBatisPlusWrapperTest {

    @Autowired
    private UserMapper userMapper;

    // 查询 条件
    @Test
    public void testWrapperQuery(){
        // 查询用户名包含a,年龄在20~30间,邮箱信息不为空
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();

        // SELECT id,name,age,email FROM user WHERE (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL)
        queryWrapper.like("name","a")
                .between("age",20,30)
                .isNotNull("email");

        List<User> list = userMapper.selectList(queryWrapper);

        list.forEach(System.out::println);
    }

    // 排序 条件
    @Test
    public void testWrapperOrder(){
        // 查询用户信息,按照年龄的降序排序(Desc),若年龄相同,则按照id升序排序。
        // SELECT id,name,age,email FROM user ORDER BY age DESC,id ASC
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();

        queryWrapper.orderByDesc("age")
                        .orderByAsc("id");

        List<User> list = userMapper.selectList(queryWrapper);

        list.forEach(System.out::println);

    }

    // 删除 条件
    @Test
    public void testWrapperDelete(){
        // 删除邮箱地址为:null
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.isNull("email");
        int result = userMapper.delete(queryWrapper);
        System.out.println(result);
    }

    // 修改 条件
    @Test
    public void testWrapperUpdate(){
        // UPDATE user SET name=?, email=? WHERE (age > ? AND name LIKE ? OR email IS NULL)
        // 将年龄大于20,并且用户名中包含a 或者 邮箱为null的用户信息修改
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.gt("age",20)
                        .like("name","a")
                        .or()
                        .isNull("email");
        User user = new User();
        user.setName("小猪");
        user.setEmail("xhan@qq.com");

        // update要求传入要修改的数据,第二个要求传入条件
        int update = userMapper.update(user,queryWrapper);
        System.out.println(update);
    }
}

条件的优先级

lambda中的条件优先执行。

and和or方法,都可以使用lambda表达式。

// lambda表达式
@Test
public void testLambda(){
    // 将用户名中含有a,并且年龄大于20 或者 邮箱为null 的用户信息修改
    // lambda中的条件优先执行
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    // i是条件构造器
    queryWrapper.like("name","a")
            .and(i->i.gt("age",20).or().isNull("email"));

    User user = new User();
    user.setName("小鹏");
    user.setEmail("666@qq.com");

    int update = userMapper.update(user, queryWrapper);
    System.out.println(update);
}

组装select

按照我们设置的字段,来进行查询

// 组装select
@Test void testWrapperSelect(){
    // 查询用户的用户名、年龄、邮箱
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.select("name","age","email");
    List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
    maps.forEach(System.out::println);
}

组装子查询

// 组装子查询
@Test
void testWrapper_Select(){
    // 查询id小于等于100的数据:
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    // inSql的第二个字段是语句
    queryWrapper.inSql("id","select id from user where id<=100");

    List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
    maps.forEach(System.out::println);
}

UpdateWrapper

之前我们都是使用queryWrapper来修改,现在我们通过UpdateWrapper也可以进行修改。

// updateWrapper
@Test
void testUpdateWrapper(){
    // 将用户名中包含a,并且年龄大于20 或者 邮箱为null 的用户信息修改
    UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();

    updateWrapper.like("name","a")
            .and(i->i.gt("age",20).or().isNull("email"));
    updateWrapper.set("name","小黑")
            .set("email","shabi@qq.com");

    // 这里不需要定义实体类,因为已经将User添加到UpdateWrapper中了
    int update = userMapper.update(null, updateWrapper);
    System.out.println(update);
}

条件组装

在真正开发的过程中,组装条件是常见的功能,而这些条件数据来源于用户输入,是可选的,因此我们在组装这些条件时,必须先判断用户是否选择了这些条件,若选择则需要组装该条件,若没有选择则一定不能组装,以免影响SQL执行的结果。

低配写法:

// 模拟企业组装条件
@Test
void testAllWrapper(){
    String name = "";
    Integer ageBegin = 20;
    Integer ageEnd = 30;
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    if(StringUtils.isNotBlank(name)) {
        // isNotBlank判断某个字符是否不为空、不为null、不为空白
        queryWrapper.like("name",name);
    }
    if(ageBegin!=null){
        queryWrapper.ge("age",ageBegin);
    }
    if(ageEnd !=null){
        queryWrapper.le("age",ageEnd);
    }
    List<User> list = userMapper.selectList(queryWrapper);
    list.forEach(System.out::println);
}

image.png

上面的实现方案没有问题,但是代码比较复杂,我们可以使用带condition参数的重载方法构建查询条件,简化代码的编写。

高配写法:

// condition组装条件
@Test
void testCondition(){
    //定义查询条件,有可能为null(用户未输入或未选择)
    String name = "a";
    Integer ageBegin = 10;
    Integer ageEnd = 24;
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.like(StringUtils.isNotBlank(name),"username", "a")
            .ge(ageBegin != null, "age", ageBegin)
            .le(ageEnd != null, "age", ageEnd);
    List<User> users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);
}

LambdaQueryWrapper

Lambda表达式需要用函数式接口来指定字段名,不可以使用字符串。

// Lambda
@Test
void testLambdaQueryWrapper(){
    String name = "a";
    Integer ageBegin = null;
    Integer ageEnd = 30;
    LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();

    // 第二个参数是 数据表列名(这里使用的写法是函数式接口写法,属性所对应的字段名),第三个参数是实体类名
    lambdaQueryWrapper.like(StringUtils.isNotBlank(name),User::getName,name)
            .ge(ageBegin!=null,User::getAge,ageBegin)
            .le(ageEnd!=null,User::getAge,ageEnd);

    List<User> list = userMapper.selectList(lambdaQueryWrapper);
    list.forEach(System.out::println);
}

LambdaUpdateWrapper

@Test
void testLambdaUpdateWrapper(){
    LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();

    // 第二个参数是 数据表列名(这里使用的写法是函数式接口写法,属性所对应的字段名),第三个参数是实体类名
    lambdaUpdateWrapper.like(User::getName,"a")
            .and(i->i.gt(User::getAge,20).or().isNull(User::getEmail));

    lambdaUpdateWrapper.set(User::getName,"张伟")
            .set(User::getAge,24)
            .set(User::getEmail,"888@qq.com");

    int result = userMapper.update(null,lambdaUpdateWrapper);
    System.out.println(result);
}

分页插件

配置类配置插件:

@Configuration
@MapperScan("xxx.mapper") // 扫描mapper接口所在的包
public class MyBatisPlusConfig {

    // 配置插件:
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 添加分页插件:并指定数据库类型(因为每个数据库的分页类型不同)
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

测试类进行测试:

@SpringBootTest
public class MyBatisPlusPluginsTest {
    @Autowired
    private UserMapper userMapper;

    @Test
    void testPage(){
        // 当前页的索引,每页显示的条数。
        Page<User> page = new Page<>(1,3);

        // 分页对象,条件构造器。返回值也是一个page
        userMapper.selectPage(page, null);

        System.out.println(page);
    }
}
System.out.println(page.getRecords()); // 获取当前页数据
System.out.println(page.getCurrent()); // 获取当前页码
System.out.println(page.getSize()); // 获取每页显示的条数
System.out.println(page.getPages()); // 获取总页数
System.out.println(page.getTotal()); // 获取总记录数
System.out.println(page.hasNext()); // 是否有下一页
System.out.println(page.hasPrevious()); // 是否有上一页

自定义分页功能

查询语句是自定义的,对我们查询出来的结果实现分页功能:自定义查询语句,就要在.xml中自己手写:

yml中配置类型别名

mybatis-plus:
    # 配置类型别名所对应的包
    type-aliases-package: com.example.atguigu_01_mybatisplus.pojo
<!-- resultType使用的User是类型别名,需要在yml中设置 -->
    <select id="selectPageVo" resultType="User">
        select id,name,age,email from user where age > #{age}
    </select>
// 第一个参数必须是一个page对象,第二个可以是自定义分页条件
// 通过年龄查询用户信息,并分页
Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);
@Test
void testMyPage(){
    Page<User> page = new Page<>(1,3);
    // 查询年龄大于20的,并进行分页
    userMapper.selectPageVo(page,20);

    System.out.println(page.getRecords()); // 获取当前页数据
    System.out.println(page.getCurrent()); // 获取当前页码
    System.out.println(page.getSize()); // 获取每页显示的条数
    System.out.println(page.getPages()); // 获取总页数
    System.out.println(page.getTotal()); // 获取总记录数
    System.out.println(page.hasNext()); // 是否有下一页
    System.out.println(page.hasPrevious()); // 是否有上一页
}

乐观锁

image.png

image.png

image.png

乐观锁会先在数据库中添加一个version字段,每次更新时,都会将version+1,如果后续的version匹配不上,则不允许操作。

@Test
void test_01_product(){
    // 小李查询商品价格
    Product productLi = productMapper.selectById(1);
    System.out.println("小李查询商品价格:"+productLi.getPrice());

    // 小王查询商品价格
    Product productWang = productMapper.selectById(1);
    System.out.println("小王查询商品价格:"+productWang.getPrice());

    // 小李将商品+50
    productLi.setPrice(productLi.getPrice()+50);
    productMapper.updateById(productLi);

    // 小王将商品-30,此时小王的操作会将小李的操作覆盖掉。
    productWang.setPrice(productWang.getPrice()-30);
    productMapper.updateById(productWang);

    // 老板查询价格:最终是70,但是预期是小李+50,150;然后-30,120,
    Product productBoss = productMapper.selectById(1);
    System.out.println("老板查询价格:"+productBoss);
}

乐观锁插件:

先给实体类中version属性加上@Version标识

@Version // 标识乐观锁版本号字段
private Integer version;

配置插件:

// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

此时得到的结果就是150了,因为小李修改后,version+1,等到小王再次操作时,version已经对不上了,所以小王的操作就会失效。所以老板查到的结果就是+50的结果。

@Test
void test_01_product(){
    // 小李查询商品价格
    Product productLi = productMapper.selectById(1);
    System.out.println("小李查询商品价格:"+productLi.getPrice());

    // 小王查询商品价格
    Product productWang = productMapper.selectById(1);
    System.out.println("小王查询商品价格:"+productWang.getPrice());

    // 小李将商品+50
    productLi.setPrice(productLi.getPrice()+50);
    productMapper.updateById(productLi);

    // 小王将商品-30,此时小王的操作会将小李的操作覆盖掉。
    productWang.setPrice(productWang.getPrice()-30);
    productMapper.updateById(productWang);

    // 老板查询价格:最终是70,但是预期是小李+50,150;然后-30,120,
    Product productBoss = productMapper.selectById(1);
    System.out.println("老板查询价格:"+productBoss);
}

优化流程

// 小王将商品-30,此时小王的操作会将小李的操作覆盖掉。
...

// 说明没有更新
if(result == 0){
    // 操作失败,重试。也就是需要重新获取新的Product
    Product productNew = productMapper.selectById(1);
    productNew.setPrice(productNew.getPrice()-30);
    productMapper.updateById(productNew);
}

// 老板查询价格:
...

多数据源

多数据源应用于:多张库,多张表共同工作,或者读写分离等。

image.png

spring:
  datasource:
    dynamic:
      # 设置默认的数据源或者数据源组,默认值是master;
      primary: master
      # 严格匹配数据源,默认false,true未匹配到指定数据源时,抛异常,false使用默认数据源
      strict: false
      datasource:
        # primary中书写的是什么,这里就得写什么
        master:
          url: jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&&useSSL=false&serverTimezone=UTC
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: root
          password: 123456
        # 从数据源
        slave_1:
          url: jdbc:mysql://localhost:3306/mybatis_plus1?characterEncoding=utf-8&&useSSL=false&serverTimezone=UTC
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: root
          password: 123456

创建:

@Repository
public interface UserMapper extends BaseMapper<User> {
}
@Repository
public interface ProductMapper extends BaseMapper<Product> {
}
public interface UserService extends IService<User> {
}
@Service
// 指定数据源
@DS("master")
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
public interface ProductService extends IService<Product> {
}
@Service
// 指定数据源
@DS("slave_1")
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
}

测试:

@Autowired
private UserService userService;

@Autowired
private ProductService productService;

@Test
void testMoreData() {
    System.out.println(userService.getById(4));
    System.out.println(productService.getById(1));
}

MyBatisX插件

这个插件是基于IDEA而生,其他编译器需要自行查找。

image.png

baomidou.com/pages/ba5b2…

image.png

// 实现自定义sql,这里只需要写方法名即可,会帮我们自动生成对应的sql语句
int insertSelective(User user);

int deleteByIdAndName(@Param("id") Long id, @Param("name") String name);

int updateAgeAndNameAndSexById(@Param("age") Integer age, @Param("name") String name, @Param("sex") Integer sex, @Param("id") Long id);

List<User> selectAgeAndSexByAgeBetween(@Param("beginAge") Integer beginAge, @Param("endAge") Integer endAge);

List<User> selectAllOrderByAgeDes();