MybatisPlus学习笔记


MybatisPlus

官网:MyBatis-Plus (baomidou.com)

=简介

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

愿景

我们的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗 中的 1P、2P,基友搭配,效率翻倍。

img

#特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

快速入门

地址:快速开始 | MyBatis-Plus (baomidou.com)

步骤

  1. 创建数据库mybatis_plus
  2. 创建user表,插入数据
CREATE TABLE USER
(
	id BIGINT(20) NOT NULL COMMENT '主键ID',
	NAME VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
	age INT(11) NULL DEFAULT NULL COMMENT '年龄',
	email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
	PRIMARY KEY (id)
)ENGINE=INNODB DEFAULT CHARSET=utf8 ;
-- 真实开发中,version(乐观锁)、deleted(逻辑删除),gmt_create(创建时间)、gmt_modifed(修改时间)

INSERT INTO USER (id, NAME, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');
  1. 编写项目,初始化项目!使用SpringBoot初始化!

  2. 导入依赖

     
     org.projectlombok
     lombok
     
     
     mysql
     mysql-connector-java
     
     
     
     com.baomidou
     mybatis-plus-boot-starter
     3.0.5
     
    

    说明:不要同时导入mybatis和mybatisplus,,可能会有版本的冲突;

  3. 连接数据库,这一步和mybatis相同

    #mysql8 驱动与mysql5不同 需要添加时区
    spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
    spring.datasource.password=onmyown123
    spring.datasource.username=root
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    

6、传统方式pojo-dao(连接数据库,配置mapper.xml)-service-controller

6、使用了mybatis-plus之后

  • pojo

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public class User {
        private Long id ;
        private String name ;
        private int age ;
        private String email;
    };
    
  • mapper

    //在对应的mapper上继承基本的类BaseMapper
    @Mapper
    public interface UserMapper extends BaseMapper {
        //Dao配置好了,直接调用父类方法就好了
    }
    
  • 测试

    @SpringBootTest
    class MybatisPlusApplicationTests {
        @Autowired
        private UserMapper userMapper;
    
        @Test
        void contextLoads() {
            List users = userMapper.selectList(null);
            users.forEach(System.out::println);
        }
    
    }
    

sql和方法mybatisPlus帮我们写好了

上述入门案例很鲜明地吐出了内置mapper的优点,强大的CRUD操作

配置日志

我们现在的sql和方法都不可见,我们希望知道它是怎么执行的,所以我们要看日志!

#配置日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

CRUD扩展

插入操作

insert插入

@Test
void testInsert(){
    User user = new User();
    user.setName("冯宝宝");
    user.setAge(100);
    user.setEmail("123456@qq.com");
    int insert = userMapper.insert(user);
    System.out.println(insert);
    System.out.println(user);
}

测试:

(这里需要注意User实体类的id参数类型是Long(long不是包装类,MybatisPlus会自动补0))

我们发现id被自动补了一个很长的数字

数据库插入的id的默认值为:全局的唯一id

主键生成策略

分布式系统唯一id生成:

雪花算法

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。

主键自增

我们需要配置主键自增

  1. 数据库表种的id改为自增

  2. 在实体类id上添加@TableId(type = IdType.AUTO)

  3. 再次测试插入

    这样设置后,不需要设置主键id,mybatisPlus也能帮助我们自增id

其它源码解释

public enum IdType {
    AUTO, //数据库id自增
    NONE, //未设置主键
    INPUT, //手动输入
    ID_WORKER, //默认的全局唯一id  数字表示
    UUID, //全局唯一id uuid
    ID_WORKER_STR;//默认的全局唯一id 字符串表示

    private int key;

    private IdType(int key) { /* compiled code */ }

    public int getKey() { /* compiled code */ }
}

更新操作

update

@Test
void testUpdate(){
    User user = new User();
    user.setId(6L);
    user.setName("冯宝宝");
    user.setAge(100);
    int i = userMapper.updateById(user);
}

测试结果:

自动完成动态sql设置!

自动填充

创建时间、修改时间,这些个操作一律都是自动完成的,我们不希望手动更新

阿里巴巴开发手册:所有的数据库表:gtm_create,gtm_modified几乎所有的表都要配置上!而且需要自动化!

数据库级别(工作中不允许修改数据库的)

  1. 在user表种新增create_time,update_time字段

  1. 再次测试修改方法,先把实体类同步

    @Test
    void testUpdate(){
        User user = new User();
        user.setId(6L);
        user.setName("阮丰");
        user.setAge(80);
        int i = userMapper.updateById(user);
        System.out.println(i);
    }
    

    然后update_time在修改代码执行后自动更新了!

代码级别

  1. 先将数据库create_time,update_time的默认值去掉,再将update_time的更新去掉

  1. 在实体类的字段上增加注解@TableFiled
//字段添加填充内容
@TableField(fill = FieldFill.INSERT)
private Date createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
  1. 编辑处理器来处理这些个注解即可~
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    //插入操作的自动填充
    @Override
    public void insertFill(MetaObject metaObject) {
        //default MetaObjectHandler setFieldValByName(String fieldName, Object fieldVal, MetaObject metaObject)
        log.info("start insertFill");
        this.setFieldValByName("createTime",new Date(),metaObject);
        this.setFieldValByName("updateTime",new Date(),metaObject);
    }
    //更新操作的自动填充
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start updateFill");
        this.setFieldValByName("updateTime",new Date(),metaObject);
    }
}
  1. 测试插入

  2. 测试更新,观察时间!

乐观锁

乐观锁:做什么事都不会出现问题,无论干什么都不会去上锁,等到出现问题了再去更新值测试

悲观锁:做什么事都会出现问题,无论干什么都会去上锁,

乐观锁实现方式:

  • 取出记录时,获取当前version
  • 更新时,带上这个version
  • 执行更新时, set version = newVersion where version = oldVersion
  • 如果version不对,就更新失败

乐观锁理解:

-- 假设有两个线程
-- A:
update user set name="abc",set version=2 
where id=2 and version=1;
-- B:
update user set name="abc",set version=2 
where id=2 and version=1;
-- 假设B线程抢先完成,version=2,那么A线程就会失败

乐观锁实现

  1. 数据库添加version字段,设置默认值为1

  2. 实体类添加version字段

    @Version
    private Integer version;
    
  3. 注册组件

    //扫描Mapper包
    @MapperScan("com.yfm.mapper")
    @EnableTransactionManagement
    @Configuration
    public class MybatisPlusConfig {
        //注册乐观锁插件
        @Bean
        public OptimisticLockerInterceptor optimisticLockerInterceptor() {
            return new OptimisticLockerInterceptor();
        }
    }
    
  4. 测试一下!

    //测试乐观锁成功
    @Test
    void testOptimisticLocker(){
        User user = userMapper.selectById(1L);
        user.setName("通天箓");
        userMapper.updateById(user);
    }
    
    //测试乐观锁失败,多线程
    @Test
    void testOptimisticLocker2(){
        //线程A
        User user2 = userMapper.selectById(1L);
        user2.setName("通天箓22");
    
        //线程B,插队更新
        User user3 = userMapper.selectById(1L);
        user3.setName("通天箓33");
    
        userMapper.updateById(user3);
    //当userMapper.updateById(user3);执行完以后,version=3,而查询出来的version=2,所以下面会失败
        userMapper.updateById(user2);
    }
    
    

    5.测试结果

    ==>  Preparing: UPDATE user SET name=?, age=?, email=?, version=?, create_time=?, update_time=? WHERE id=? AND version=? 
    ==> Parameters: 通天箓33(String), 18(Integer), test1@baomidou.com(String), 3(Integer), 2021-11-21 17:04:33.0(Timestamp), 2021-11-22 09:22:57.102(Timestamp), 1(Long), 2(Integer)
    <==    Updates: 1
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7afb9c93]
    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@f4f843f] was not registered for synchronization because synchronization is not active
    2021-11-22 09:22:57.116  INFO 17600 --- [           main] com.yfm.handler.MyMetaObjectHandler      : start updateFill
    JDBC Connection [HikariProxyConnection@2069378030 wrapping com.mysql.cj.jdbc.ConnectionImpl@6eed46e9] will not be managed by Spring
    ==>  Preparing: UPDATE user SET name=?, age=?, email=?, version=?, create_time=?, update_time=? WHERE id=? AND version=? 
    ==> Parameters: 通天箓22(String), 18(Integer), test1@baomidou.com(String), 3(Integer), 2021-11-21 17:04:33.0(Timestamp), 2021-11-22 09:22:57.116(Timestamp), 1(Long), 2(Integer)
    <==    Updates: 0
    

查询操作

@Test
void testSelectById(){
    User user = userMapper.selectById("1L");
    System.out.println(user);
}
//测试批量查询
@Test
void testSelectByBatchIds(){
    List users = userMapper.selectBatchIds(Arrays.asList(1, 6));
    users.forEach(System.out::println);

}
//测试Map多条件查询
@Test
void testSelectByMap(){
    HashMap map = new HashMap();
    map.put("name","无根生");
    map.put("age",70);
    List users = userMapper.selectByMap(map);
    users.forEach(System.out::println);
}

分页查询

分页在网站上用的非常多

  • 原始limit
  • PageHelper第三方分页插件-
  • MP其实也内置了分页插件!

如何使用

  1. 配置拦截器组件即可、

    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
        // paginationInterceptor.setOverflow(false);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        // paginationInterceptor.setLimit(500);
    
        // 开启 count 的 join 优化,只针对部分 left join
        //paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
        return paginationInterceptor;
    }
    
  2. 直接使用Page对象即可!

//测试分页
@Test
void testPage(){
    //参数1:当前页
    //参数2:每页大小
    Page userPage = new Page<>(1,5);
    userMapper.selectPage(userPage,null);
    //打印输出
    userPage.getRecords().forEach(System.out::println);
    //提示:分页操作之前都会先查询表中记录总数
}

删除操作

基本的删除操作:

deleteById
deleteBatchIds
deleteByMap

逻辑删除

物理删除:从数据库中直接删除

逻辑删除:在数据库中没有被移除,而是通过一个变量让他失效!deleted=0 => deleted=1

管理员可以查看被删除的记录!防止数据的丢失,相当于回收站的作用。如果真要删除,再通过deleted=1删除数据

测试一下:

1、在数据库中添加deleted字段

2、在实体类中新增deleted属性

@TableLogic  //逻辑删除
private Integer deleted;

3、逻辑删除配置,注册组件

//从MP3.1.1版本起就不需要注入了
//逻辑删除组件
@Bean
public ISqlInjector sqlInjector(){
    return  new LogicSqlInjector();
}
#配置逻辑删除
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0

4、测试删除

@Test
void testDelete(){
    int i = userMapper.deleteById(1462326539445100546L);
    System.out.println(i);
}

结果:删除操作转变为了更新操作,修改deleted字段!

此时再查询就查不到了,在查询时会加入deleted=0条件!

性能分析插件

我们在平时的开发中,会遇到一些慢sql。

MP也提供性能分析插件,如果超过这个时间就停止

步骤:

1、导入组件

//该组件在新版本被移除了
@Bean
@Profile({"dev","test"}) //设置dev test环境开启,保证我们的效率
public PerformanceInterceptor performanceInterceptor(){
    PerformanceInterceptor performanceInterceptor=new PerformanceInterceptor();
    performanceInterceptor.setMaxTime(100);//ms   设置sql执行的最大时间,如果超过了就不执行
    performanceInterceptor.setFormat(true);  //sql格式化打印
    return  performanceInterceptor;
}

注意在application.properties中添加开发环境设置

#设置为开发环境
spring.profiles.active=dev

2、测试使用!

条件构造器

十分重要:Wrapper

我们写一些复杂的sql就可以用它来替代!

测试:

测试1:

@Test
void contextLoads() {
    //注意要加泛型,不加就不能链式增加条件了!
    QueryWrapper wrapper = new QueryWrapper<>();
    //测试查询年龄大于30,邮箱,姓名不为空的记录
    wrapper
        .isNotNull("email")
        .isNotNull("name")
         //ge:大于等于。
        .ge("age",30);  //和我们刚才学的Map对比一下
    userMapper.selectList(wrapper).forEach(System.out::println);

}

测试2:

@Test
void test2() {
    //查询年龄等于18的记录
    QueryWrapper wrapper = new QueryWrapper<>();
    //注意要加泛型,不加就不能链式增加条件了!
    wrapper
        .eq("age",18);
    User user = userMapper.selectOne(wrapper);//查询一个数据。出现多个结果使用List或Map
    System.out.println(user);
}

测试3:

@Test
void test3() {
    //查询年龄在18——50之间的结果数
    QueryWrapper wrapper = new QueryWrapper<>();
    //注意要加泛型,不加就不能链式增加条件了!
    wrapper
        .between("age",18,50);
    Integer count = userMapper.selectCount(wrapper);
    System.out.println(count);
}

测试4:记住,查看输出的SQL进行分析

//模糊查询
@Test
void test4() {
    //注意要加泛型,不加就不能链式增加条件了!
    QueryWrapper wrapper = new QueryWrapper<>();
    //这里like的right、left的意思是'%'在right,还是left
    wrapper.likeRight("email","t")    //查询邮箱以t开头的记录
        .notLike("name","t");   //名字不含t
    List> maps = userMapper.selectMaps(wrapper);
    maps.forEach(System.out::println);
}
-- Execute SQL:
    SELECT
        id,
        name,
        age,
        email,
        version,
        deleted,
        create_time,
        update_time 
    FROM
        user 
    WHERE
        deleted=0 
        AND email LIKE 't%' 
        AND name NOT LIKE '%t%'

测试5:

//子查询
@Test
void test5() {
    //注意要加泛型,不加就不能链式增加条件了!
    QueryWrapper wrapper = new QueryWrapper<>();
    //id在子查询中查出来
    wrapper.inSql("id","select id from user where id>3");
    List objects = userMapper.selectObjs(wrapper);
    objects.forEach(System.out::println);
}

结果:

Execute SQL:
    SELECT
        id,
        name,
        age,
        email,
        version,
        deleted,
        create_time,
        update_time 
    FROM
        user 
    WHERE
        deleted=0 
        AND id IN (
            select
                id 
            from
                user 
            where
                id>3
        )

测试6:

//排序
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.orderByAsc("age");

关于条件构造器的内容还有很多,比如分组等等,作为课后练习

代码生成器

mapper ,pojo,service,controller都帮你写!

使用下面代码前提:

导入MybatisPlus依赖,狂神视频用的是3.0.5版本的

初次之外,还要导入模板引擎依赖



    com.baomidou
    mybatis-plus-boot-starter
    3.0.5



    org.apache.velocity
    velocity-engine-core
    2.3

public static void main(String[] args) {
//需要构建一个 代码自动生成器 对象
    AutoGenerator mpg = new AutoGenerator();
    //配置策略
    //1 全局配置
    GlobalConfig gc = new GlobalConfig();
    //D:\java\idea\IntelliJ IDEA 2020.1\IDEACode\mybatisPlus\mybatis_plus
    String projectPath = System.getProperty("user.dir");//代表项目的当前路径
    gc.setOutputDir(projectPath+"/src/main/java");
    gc.setAuthor("yfm");
    gc.setOpen(false);//是否打开资源管理器
    gc.setFileOverride(false); //是否覆盖原来的代码
    gc.setServiceName("%sService"); //去Service的I前缀
    gc.setIdType(IdType.ID_WORKER);
    gc.setDateType(DateType.ONLY_DATE);
    gc.setSwagger2(true);
    mpg.setGlobalConfig(gc);

    //2 数据源配置
    DataSourceConfig dsc = new DataSourceConfig();
    dsc.setUrl("jdbc:mysql://localhost:3306/db1?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8");
    dsc.setUsername("root");
    dsc.setPassword("onmyown123");
    dsc.setDriverName("com.mysql.cj.jdbc.Driver");
    dsc.setDbType(DbType.MYSQL);
    mpg.setDataSource(dsc);

    //3 包的设置
    PackageConfig pc= new PackageConfig();
    pc.setModuleName("blog");//磨块命
    pc.setParent("com.parent");//包名   结果就是 com.parent.blog
    pc.setEntity("entity");//实体类包名
    pc.setMapper("mapper");
    pc.setService("service");
    pc.setController("controller");
    mpg.setPackageInfo(pc);

    //4 策略配置 ,逻辑删除,乐观锁等等
    StrategyConfig strategy = new StrategyConfig();
    strategy.setInclude("user");//设置要映射的表名
    strategy.setNaming(NamingStrategy.underline_to_camel);
    strategy.setColumnNaming(NamingStrategy.underline_to_camel);
    strategy.setEntityLombokModel(true); //自动lombok
    //逻辑删除
    strategy.setLogicDeleteFieldName("deleted");
    //乐观锁
    strategy.setVersionFieldName("version");
    //自动填充
    TableFill createTime =new TableFill("createTime", FieldFill.INSERT);
    TableFill updateTime =new TableFill("updateTime", FieldFill.INSERT_UPDATE);
    ArrayList tableFills = new ArrayList<>();
    tableFills.add(createTime);
    tableFills.add(updateTime);
    strategy.setTableFillList(tableFills);

    strategy.setRestControllerStyle(true);
    strategy.setControllerMappingHyphenStyle(true);  //localhost:8080/hello_id_2
    mpg.setStrategy(strategy);

    mpg.execute();//执行

}