(六)MyBatis Plus

379 阅读11分钟

简介

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑。

只需简单配置,即可快速进行 CRUD 操作,从而节省大量时间。

热加载、代码生成、分页、性能分析等功能一应俱全。

如何使用

导入依赖

注意**:**引入 MyBatis-Plus 之后请不要再次引入 MyBatis,以避免因版本差异导致的问题。

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <!--mybatis-plus-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.1</version>
    </dependency>

    <!--mysql运行时依赖-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!--lombok用来简化实体类-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

application.yml

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

这里在url后面加上?serverTimezone=GMT%2B8是因为8.0版本的jdbc驱动需要添加这个后缀,否则运行测试用例报告如下错误:

java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more 。

而且也可以发现这里的驱动不再是com.mysql.jdbc.Driver了,也是由于jdbc 8的版本,需要改用com.mysql.cj.jdbc.Driver.

然后,按步骤加上主启动类(需要加上@MapperScan注解去加载mapper)、实体类。

mapper接口我们可以直接继承BasedMapper<表对应实体类>,就可以使用了 。

这里注意,

IDEA在 userMapper 处报错,因为找不到注入的对象,因为类是动态创建的,但是程序可以正确的执行。

为了避免报错,可以在dao层的接口上添加 @Repository 注解,或者使用@Resource 替换@Repository。

比如如下格式。

@Repository
public interface UserMapper extends BaseMapper<User> {
    
}

如果想要测试SpringBoot工程,可以在测试类上加上@SpringBootTest注解,就可以正常注入bean了。然后测试方法上加上@Test。

数据库分库分表策略

背景

​ 随着业务规模的不断扩大,需要选择合适的方案去对应数据规模的增长,以应对主键增长的访问量。

​ 数据库的扩展方式主要包括:业务分库、主从复制、数据库分表。

1.业务分库

​ 业务分库指的是按照业务模块将数据分散到不同的数据库服务器。例如:电商网站,包括用户,商品、订单三个业务模块,我们可以将用户数据,商品数据和订单数据分开放在三台不同的数据库服务器上,而不是将所有数据都放在同一台数据库服务器上,这样就变成了3个数据库同时承担压力,系统的吞吐量自然就高了。

产生的新问题:

  1. join操作问题。业务分库之后,原本在一个数据库里的表分散到不同的库里,就会导致无法使用sql 的join查询
  2. 事务问题。原本在同一个数据库中的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改了。
  3. 成本问题。需要增加服务器。

2.主从复制,读写分离

读写分离的基本原理:

​ 将数据库读写操作分散到不同的节点上。

如何实现:

  1. 数据库服务器搭建主从集群,一主一从,一主多从都可以。
  2. 数据库的主机负责读写,从机只负责读操作。
  3. 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
  4. 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。

注意:“主从”和“主备”中的从机和备用机的功能有所不同,从机需要提供读数据的功能,而备机一般被认为只提供备份功能,不提供访问功能,所以使用“主从”还是“主备”是需要看场景的。

3.数据库分表

​ 单表数据拆分有两种方式:垂直分表和水平分表。

​ 单表进行切分后,是否要将切分后的多个表分散在不同的数据库服务器中,可以提供实际的切分效果来确定。如果性能能够哦满足业务要求,是可以不拆分到堕胎数据库服务器的,因为业务分库会带来很多复杂的问题。分表能够有效的分散存储压力,提升性能,但是和分库一样,会引入各种复杂性。

垂直分表:

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

水平分表:

  1. 水平分表适合表行数特别大的表。当表的数据量达到千万级别时(当然也和表的字段数量有关),就要考虑是否进行分表了,因为这很可能是架构的性能瓶颈或者隐患。

水平分表相比垂直分表,会引入更多复杂的问题,比如主键id。

解决方案1——主键自增

​ 以最常见的用户 ID 为例,可以按照 1000000 的范围大小进行分段,1 ~ 999999 放到表 1中,1000000 ~ 1999999 放到表2中,以此类推。

​ 这个解决方案的问题在于分段大小的选取,分段太小会导致后子表数据量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在100万到2000万之间,具体需要根究业务选取合适的分段大小。

​ 优点是:可以随着数据的增加平滑的扩充新的表。例如,现在的用户是100万,如果增加到1000万,只需要增加新的表就可以了,原有的数据不需要动。

解决方案2——Hash

​ 同样以用户 ID 为例,假如我们一开始就规划了 10 个数据库表,路由算法可以简单地用 user_id % 10 的值来表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID 为 10086 的用户放到编号为 6 的字表中。

复杂点:初始表数量的选取,表数量太多维护困难,数量太少可能会导致单表性能存在问题。

优点:表分布的比较均匀

缺点:扩充新的表比较麻烦,所有数据都要重新分布。

ID生成器:雪花算法snowflake

雪花算法是Twitter公布的分布式主键生成算法,它能够保证不同表的主键不重复性,以及相同表的主键的有序性。

核心思想:

  1. 长度一共64bit(long)
  2. 最前一个符号位,1bit,由于long在java中是带符号的,最高位是符号位,id一般是正数,最高位是0.
  3. 41bit的时间戳(毫秒级),存储的是时间戳的差值(当前时间戳-开始时间戳)。
  4. 10bit,是机器的id(其中5bit是数据中心,5bit是机器ID,可以部署在1024个节点)
  5. 12bit,毫秒内的流水号,也就意味着每个节点在每毫秒内可以产生4096个ID

优点:整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率比较高

MyBatis-Plus(MP)的主键策略

@TableId注解:指定主键策略,声明于类变量上。

1.ASSIGN_ID

MP默认的主键策略是ASSIGN_ID,也就是使用了雪花算法。

@TableId(type = IdType.ASSIGN_ID)
private String id;

2.AUTO自增策略

需要在创建数据表的时候设置主键自增。

@TableId(type = IdType.AUTO)
private Long id;

如果想要影响所有实体的配置,可以设置全局主键配置

#全局设置主键生成策略
mybatis-plus.global-config.db-config.id-type=auto

MP的自动填充功能

​ 在项目中会遇到一些数据,每次都使用相同的方式填充,例如记录的创建时间,更新时间等。就可以使用MP的自动填充功能。

@TableField指定普通字段策略。

在注解中传入fill参数(用FieldFill中的字段填写)

  @TableField(fill = FieldFill.INSERT)
    private Date gmtCreate;

    //@TableField(fill = FieldFill.UPDATE)
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date gmtModified;

如何实现乐观锁

  1. 在表中添加version字段,取出记录时也要获取当前的version,每次更新version就+1,如果where中version版本不对,就让这次的更新失败。

  2. 修改实体类,在version属性上加@Version注解。

  3. 创建配置文件MybatisPlusConfig,这时可以删除主类中的@MapperScan扫描注解

    
    @EnableTransactionManagement
    @Configuration
    @MapperScan("com.dyy.mybatisplus.mapper")
    public class MybatisPlusConfig {
        
    }
    
  4. 注册乐观锁插件(其实就是注册一个bean)

    /**
         * 乐观锁插件
         */
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
        return new OptimisticLockerInterceptor();
    }
    

查询

1.通过多个id批量查询

mapper继承自BaseMapper接口中有已经实现的方法selectBatchIds(集合)。

@Test
public void testSelectBatchIds(){

    List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
    users.forEach(System.out::println);
}

2.简单的条件查询

通过map封装查询条件。

方法:selectByMap(Map map);

注意map中的key必须完全对应数据库中的列名

@Test
public void testSelectByMap(){

    HashMap<String, Object> map = new HashMap<>();
    map.put("name", "Helen");
    map.put("age", 18);
    List<User> users = userMapper.selectByMap(map);

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

3.查询指定的列

当指定了特定的查询列时,希望分页结果只返回被查询的列,而不是很多null值。

测试selectMapsPage分页:结果集是Map

@Test
public void testSelectMapsPage() {

    //这种方式返回很多null列
    //Page<User> page = new Page<>(1, 5);
    //QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    //指定查询的列
    //queryWrapper.select("name", "age");
    //Page<User> pageParam = userMapper.selectPage(page, queryWrapper);
    //
    //pageParam.getRecords().forEach(System.out::println);

    Page<Map<String, Object>> page = new Page<>(1, 5);
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.select("name", "age");
    Page<Map<String, Object>> pageParam = userMapper.selectMapsPage(page, queryWrapper);

    List<Map<String, Object>> records = pageParam.getRecords();
    records.forEach(System.out::println);
    System.out.println(pageParam.getCurrent());
    System.out.println(pageParam.getPages());
    System.out.println(pageParam.getSize());
    System.out.println(pageParam.getTotal());
    System.out.println(pageParam.hasNext());
    System.out.println(pageParam.hasPrevious());
}

分页

MP自带分页插件。

  1. 添加分页插件,配置类中添加@Bean配置

    /**
     * 分页插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
    
  2. 测试selectPage分页

    @Test
    public void testSelectPage() {
    
        Page<User> page = new Page<>(1,5);
        Page<User> pageParam = userMapper.selectPage(page, null);
    
        pageParam.getRecords().forEach(System.out::println);
        System.out.println(pageParam.getCurrent());
        System.out.println(pageParam.getPages());
        System.out.println(pageParam.getSize());
        System.out.println(pageParam.getTotal());
        System.out.println(pageParam.hasNext());
        System.out.println(pageParam.hasPrevious());
    }
    

控制台sql语句打印:SELECT id,name,age,email,create_time,update_time FROM user LIMIT 0,5

例子:

service接口中有如下业务

    IPage<Teacher> selectPage(Long page, Long limit, TeacherQuery teacherQuery);

实现类(加上注解@Service,否则controller层调用无法自动注入)。


@Service
public class TeacherServiceImpl extends ServiceImpl<TeacherMapper, Teacher> implements TeacherService {

    @Override
    public IPage<Teacher> selectPage(Long page, Long limit, TeacherQuery teacherQuery) {
        
        Page<Teacher> pageParam = new Page<>(page, limit);
        
        QueryWrapper<Teacher> queryWrapper = new QueryWrapper<>();
        queryWrapper.orderByAsc("sort");

        if (teacherQuery == null){
            return baseMapper.selectPage(pageParam, queryWrapper);
        }

        String name = teacherQuery.getName();
        Integer level = teacherQuery.getLevel();
        String begin = teacherQuery.getJoinDateBegin();
        String end = teacherQuery.getJoinDateEnd();

        if (!StringUtils.isEmpty(name)) {
            //左%会使索引失效
            queryWrapper.likeRight("name", name);
        }

        if (level != null) {
            queryWrapper.eq("level", level);
        }

        if (!StringUtils.isEmpty(begin)) {
            queryWrapper.ge("join_date", begin);
        }

        if (!StringUtils.isEmpty(end)) {
            queryWrapper.le("join_date", end);
        }

        return baseMapper.selectPage(pageParam, queryWrapper);
    }
}

controller,最后传给前端的是一个IPage对象

@ApiOperation("分页讲师列表")
@GetMapping("list/{page}/{limit}")
public R listPage(@ApiParam(value = "当前页码", required = true) @PathVariable Long page,
                  @ApiParam(value = "每页记录数", required = true) @PathVariable Long limit,
                  @ApiParam("讲师列表查询对象") TeacherQuery teacherQuery){
    IPage<Teacher> pageModel = teacherService.selectPage(page, limit, teacherQuery);
    return  R.ok().data("pageModel", pageModel);
}

删除

1.根据id删除

方法:deleteById(long id)

@Test
public void testDeleteById(){

    int result = userMapper.deleteById(5L);
    System.out.println(result);
}

2.批量删除

方法:deleteBatchIds(List list)

@Test
public void testDeleteBatchIds() {

    int result = userMapper.deleteBatchIds(Arrays.asList(8, 9, 10));
    System.out.println(result);
}

3.简单条件删除

方法:deleteByMap(map)

@Test
public void testDeleteByMap() {

    HashMap<String, Object> map = new HashMap<>();
    map.put("name", "Helen");
    map.put("age", 18);

    int result = userMapper.deleteByMap(map);
    System.out.println(result);
}

4.逻辑删除

一般来说,我们将删除分类为物理删除和逻辑删除

  • 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到这条被删除的数据。
  • 逻辑删除:假删除,将对应数据中代表是否被删除字段的状态改为“被删除状态”,之后再数据库中仍旧能够看到这条数据记录。

逻辑删除的使用场景:

  1. 可以进行数据恢复
  2. 有关联数据,也不方便删除

实现流程

  1. 表添加字段deleted,布尔类型,默认值为false(数据库中boolean值会默认转为0或1,false->0,true->1)

  2. 实体类修改,添加上deleted字段,并且再字段上加上@TabelLogic注解(标记为逻辑删除)

  3. 可以改动下面的配置,这个配置是MP中的默认配置,如果和它一样,就不用改了。主要是给MP识别和区分数据的删除状态的。

    mybatis-plus.global-config.db-config.logic-delete-value=1
    mybatis-plus.global-config.db-config.logic-not-delete-value=0
    

配置完成后再进行测试删除方法,查看数据库数据是没有被删除的,deleted字段的值会由0变1.所以这个时候实际上进行的语句是update。

删除后再进行select操作,被标记为已删除的字段也不会被查出来。

注意,被删除前,数据的delete字段的值必须是0,才能被选取出来执行逻辑删除的。

Wapper类

是一个条件构造抽象类。用于封装查询和更新条件。

它的子类包括:

  1. AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
  2. QueryWrapper : 查询条件封装(常用)
  3. UpdateWrapper : Update 条件封装
  4. AbstractLambdaWrapper : 使用Lambda 语法
  5. LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper
  6. LambdaUpdateWrapper : Lambda 更新封装Wrapper

方法

查询方式说明
setSqlSelect设置 SELECT 查询字段
whereWHERE 语句,拼接 + WHERE 条件
andAND 语句,拼接 + AND 字段=值
andNewAND 语句,拼接 + AND (字段=值)
orOR 语句,拼接 + OR 字段=值
orNewOR 语句,拼接 + OR (字段=值)
eq等于=
allEq基于 map 内容等于=
ne不等于<>
gt大于>
ge大于等于>=
lt小于<
le小于等于<=
like模糊查询 LIKE
notLike模糊查询 NOT LIKE
inIN 查询
notInNOT IN 查询
isNullNULL 值查询
isNotNullIS NOT NULL
groupBy分组 GROUP BY
havingHAVING 关键词
orderBy排序 ORDER BY
orderAscASC 排序 ORDER BY
orderDescDESC 排序 ORDER BY
existsEXISTS 条件语句
notExistsNOT EXISTS 条件语句
betweenBETWEEN 条件语句
notBetweenNOT BETWEEN 条件语句
addFilter自由拼接 SQL
last拼接在最后,例如:last(“LIMIT 1”)

都可以在QueryWrapper的对象中调用到这些方法,然后将条件参数传进去就可以了。然后将包装好的QueryWrapper对象传给selectMaps就可以查出数据了。

MP代码生成器

转自官方文档:baomidou.com/pages/779a6…

1.依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.1</version>
</dependency>

注意:这个包没有传递依赖MP包,需要自己引入。

2.生成

快速生成:

FastAutoGenerator.create("url", "username", "password")
    .globalConfig(builder -> {
        builder.author("baomidou") // 设置作者
            .enableSwagger() // 开启 swagger 模式
            .fileOverride() // 覆盖已生成文件
            .outputDir("D://"); // 指定输出目录
    })
    .packageConfig(builder -> {
        builder.parent("com.baomidou.mybatisplus.samples.generator") // 设置父包名
            .moduleName("system") // 设置父包模块名
            .pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D://")); // 设置mapperXml生成路径
    })
    .strategyConfig(builder -> {
        builder.addInclude("t_simple") // 设置需要生成的表名
            .addTablePrefix("t_", "c_"); // 设置过滤表前缀
    })
    .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
    .execute();

也可以使用交互式生成

FastAutoGenerator.create(DATA_SOURCE_CONFIG)
    // 全局配置
    .globalConfig((scanner, builder) -> builder.author(scanner.apply("请输入作者名称?")).fileOverride())
    // 包配置
    .packageConfig((scanner, builder) -> builder.parent(scanner.apply("请输入包名?")))
    // 策略配置
    .strategyConfig((scanner, builder) -> builder.addInclude(getTables(scanner.apply("请输入表名,多个英文逗号分隔?所有输入 all")))
                        .controllerBuilder().enableRestStyle().enableHyphenStyle()
                        .entityBuilder().enableLombok().addTableFills(
                                new Column("create_time", FieldFill.INSERT)
                        ).build())
    /*
        模板引擎配置,默认 Velocity 可选模板引擎 Beetl 或 Freemarker
       .templateEngine(new BeetlTemplateEngine())
       .templateEngine(new FreemarkerTemplateEngine())
     */
    .execute();


// 处理 all 情况
protected static List<String> getTables(String tables) {
    return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));
}