MyBatis-Plus——实践篇 MyBatis-Plus——实践篇

MyBatis-Plus (简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。进行数据库操作常用的第三方组件。

目前主流的持久层框架:

JPA (springboot用得多)、 tk-mapper(通用框架用的多)、MyBatisPlus

一句话概括:MyBatis 本来就是简化 JDBC 操作的,而MyBatis-Plus用于简化 MyBatis 操作。

特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作。BaseMapper<对象泛型>进行操作。
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作(简单的CRUD操作不需要再编写xml语句),更有强大的条件构造器,满足各类使用需求
  • 支持 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 进行 crud, 并且支持标准 sql 的数据库

框架结构

MyBatis-Plus——实践篇
MyBatis-Plus——实践篇

快速入门

使用第三方组件一般通用步骤:前三步是基础入门,最后一步是使用组件的扩展技术。
1、导入对应的依赖
2、研究依赖如何配置
3、代码如何编写
4、提高扩展技术能力

项目准备:

1、数据库创建:
MyBatis-Plus——实践篇
MyBatis-Plus——实践篇

2、表和数据:

DROP TABLE IF EXISTS 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 '邮箱',
version int(5) NULL DEFAULT NULL COMMENT '版本号',
gmt_create datetime NULL DEFAULT NULL COMMENT '创建时间',
gmt_modified datetime NULL DEFAULT NULL COMMENT '修改时间',
delete_flag int(1) NULL DEFAULT NULL COMMENT '是否删除 0:否,1:是',
PRIMARY KEY (id)
);


INSERT INTO user (id, name, age, email,version,gmt_create,gmt_modified,delete_flag) VALUES
(1, 'Jone', 18, 'test1@baomidou.com',1,now(),null,0),
(2, 'Jack', 20, 'test2@baomidou.com',1,now(),null,0),
(3, 'Tom', 28, 'test3@baomidou.com',1,now(),null,0),
(4, 'Sandy', 21, 'test4@baomidou.com',1,now(),null,0),
(5, 'Billie', 24, 'test5@baomidou.com',1,now(),null,0);

3、idea插件:lombok,简化bean对象的getter/setter、构造器等的编写。

4、创建springboot项目

5、导入依赖

	<dependencies>
<!--		mysql数据库驱动-->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
<!--		lombok依赖-->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
<!--		mybatis-plus依赖-->
	<!--	使用 mybatis-plus 可以节省我们大量的代码,因为!版本的差异,尽量不要同时导入 mybatis 和 mybatisplus-->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.0.5</version>
		</dependency>
		<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>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
	</dependencies>

6、连接mysql数据库

# mysql 5 驱动不同 com.mysql.jdbc.Driver
#useSSL=true 表示是否使用安全连接,mac如果报错就改为false
#useUnicode=true&characterEncoding=utf-8 表示是否使用字符集编码并给定字符集编码类型utf-8
#serverTimezone=GMT%2B8 时区的配置,mysql8使用
spring:
    datasource:
        username: root
        password: 123456
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/mybatis_plus?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
# mysql 8 驱动不同com.mysql.cj.jdbc.Driver(虽然驱动不同,但是这个驱动是向上兼容mysql5的)、需要增加时区的配置,serverTimezone=GMT%2B8
#spring:
#    datasource:
#        username: root
#        password: 123456
#        driver-class-name: com.mysql.cj.jdbc.Driver
#        url: jdbc:mysql://localhost:3306/mybatis_plus?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8

7、使用mybatis-plus进行单元测试

示例代码:

pojo:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Integer version;
    private LocalDateTime gmtCreate;
    private LocalDateTime gmtModified;
    private Integer deleteFlag;
}

dao或者mapper接口:

//可以使用@Mapper或@Repository代表持久层,注入spring容器
//@Mapper
@Repository
public interface UserDao extends BaseMapper<User> {
// 在对应的Mapper上面继承基本的类 BaseMapper,所有的CRUD操作都已经编写完成了,直接使用BaseMapper上定义的方法
}

启动类上去扫描我们的mapper包下的所有接口:@MapperScan("com.kuang.mapper")

//启动类上去扫描我们的dao包下的所有接口
@MapperScan("com.wayne.dao")
@SpringBootApplication
public class MybatisPlusApplication {
	public static void main(String[] args) {
		SpringApplication.run(MybatisPlusApplication.class, args);
	}
}

测试类:

@SpringBootTest
class MybatisPlusApplicationTests {
	// 继承了BaseMapper,所有的方法都来自己父类
// 我们也可以编写自己的扩展方法!
	@Autowired
	private UserDao userDao;

	@Test
	public void testQuery(){
		//参数是一个 Wrapper,条件构造器,这里我们先不用 null,默认查询全部用户
		List<User> users = userDao.selectList(null);
		users.forEach(System.out::println);
	}
}

结果:

User(id=1, name=Jone, age=18, email=test1@baomidou.com, version=1, gmtCreate=2020-12-15T23:39:47, gmtModified=null, deleteFlag=0)
User(id=2, name=Jack, age=20, email=test2@baomidou.com, version=1, gmtCreate=2020-12-15T23:39:47, gmtModified=null, deleteFlag=0)
User(id=3, name=Tom, age=28, email=test3@baomidou.com, version=1, gmtCreate=2020-12-15T23:39:47, gmtModified=null, deleteFlag=0)
User(id=4, name=Sandy, age=21, email=test4@baomidou.com, version=1, gmtCreate=2020-12-15T23:39:47, gmtModified=null, deleteFlag=0)
User(id=5, name=Billie, age=24, email=test5@baomidou.com, version=1, gmtCreate=2020-12-15T23:39:47, gmtModified=null, deleteFlag=0)

报错:The server time zone value '�й���׼ʱ��' is unrecognized or represents more than one time zone.

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userDao' defined in file [F:waynecodemybatis_plus	argetclassescomwaynedaoUserDao.class]: Unsatisfied dependency expressed through bean property 'sqlSessionFactory'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in class path resource [com/baomidou/mybatisplus/autoconfigure/MybatisPlusAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.apache.ibatis.session.SqlSessionFactory]: Factory method 'sqlSessionFactory' threw exception; nested exception is com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Error: GlobalConfigUtils setMetaData Fail !  Cause:java.sql.SQLException: The server time zone value '�й���׼ʱ��' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the 'serverTimezone' configuration property) to use a more specific time zone value if you want to utilize time zone support.

A:这是因为我们的mysql-connector-java依赖默认的版本是mysql8的,需要配置时区。在mysql连接的url配置上增加时区配置serverTimezone=GMT%2B8。GMT%2B8表示东8区。还有需要注意的是serverTimezone不要写错成serverTimeZone。

Q:@Repository是用于修饰类,让spring扫描注册到容器中,那么接口上加@Repository有意义么?

A: 在接口上加@Repository注解没有意义

首先,如果AttachmentFileMapper不加@Repository注解的话有些版本的idea会报NoSuchBeanDefinitionException的红色下划线错误,实际是可以启动成功的,并没有什么影响。其次,@Repository注解作用是告诉spring容器这是个bean实例,需要扫描加载到容器中,但是我们用@Repository标记的AttachmentFileMapper是个接口,是无法创建bean实例的。所以在接口上加@Repository注解没有意义;最后,AttachmentFileMapper之所以只有接口而没有实现,在运行时也没有报错的原因是mybatis通过@MapperScan 扫描这些mapper接口重新注册beandefinition,对 BeanDefinition 进行了加工 processBeanDefinitions() 。当我们需要AttachmentFileMapper时会去工厂( MapperFactoryBean )里面扔了个 AttachmentFileMapper.class 的参数进去,工厂的 getObject() 方法给我们返回了它制造的 AttachmentFileMapper。

日志配置

用于开发日志打印,需要扩展slf4j接口,logback实现的日志配置。

#日志配置
mybatis-plus:
    configuration:
        #默认控制台日志输出配置StdOutImpl,如果要使用slf4j,logback,log4j2输出需要加对应组件依赖和更改log-impl实现的日志类
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

CRUD实践

插入操作

示例:

@Test
public void testInsert(){
    User user = new User();
    user.setName("大青山");
    user.setAge(20);
    user.setEmail("daqingshan@qq.com");
    int result = userDao.insert(user);//帮我们自动生成id,默认主键生成策略是ID_WORKER 全局唯一id(雪花算法),id=1339951110603628546
    System.out.println(result);//1 ,受影响的行数
    System.out.println(user);//// 注意:id会自动回填到user对象中
}

输出:

1
User(id=1339951110603628546, name=大青山, age=20, email=daqingshan@qq.com, version=null, gmtCreate=null, gmtModified=null, deleteFlag=null)

主键生成策略

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

默认 ID_WORKER 全局唯一id(即雪花算法)

分布式系统唯一ID生成方案汇总

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

主键自增

1、实体类字段上 @TableId(type = IdType.AUTO)
2、数据库字段一定要是自增,AUTO_INCREMENT

报错:主键自增时报 Field 'id' doesn't have a default value。

### Cause: java.sql.SQLException: Field 'id' doesn't have a default value
; Field 'id' doesn't have a default value; nested exception is java.sql.SQLException: Field 'id' doesn't have a default value

A:这是因为当我们设置id为主键自增时,即@TableId(type = IdType.AUTO),数据库表相应的也要勾选保存主键为自动递增,即AUTO_INCREMENT。

其他自增策略

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

更新操作

所有的sql都是自动帮你动态配置的。

示例:

 @Test
    public void testUpdate() {
        //// 通过条件自动拼接动态sql
        User user = new User();
        user.setName("大青山GREEN");
        user.setAge(210);
        user.setEmail("daqingshan@qq.com");
        //UPDATE user SET name=?, age=?, email=? WHERE id=?  注意如果id为null则执行成功但没有行数据被更新,result=0
        user.setId(9L);
        // 注意:updateById 但是参数是一个 对象!
        int result = userDao.updateById(user);
        System.out.println(result);//1 ,受影响的行数
        System.out.println(user);//注意更新后的值也会回填到user对象中 User(id=9, name=大青山GREEN, age=210, email=daqingshan@qq.com, version=null, gmtCreate=null, gmtModified=null, deleteFlag=null)
    }

自动填充

创建时间、修改时间!这些个操作一遍都是自动化完成的,我们不希望手动更新!
阿里巴巴开发手册:所有的数据库表:gmt_create、gmt_modified几乎所有的表都要配置上!而且需
要自动化!

方式一:数据库级别

数据库加入默认值,由数据库完成自动填充。

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(30) NOT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
  `version` int(5) DEFAULT NULL COMMENT '版本号',
  `gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `delete_flag` int(1) DEFAULT NULL COMMENT '是否删除 0:否,1:是',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

方式二:代码级别

这时候就不需要数据库表设计字段默认值了。

1、实体类字段属性上需要增加注解

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime gmtCreate;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime gmtModified;

2、编写处理器来处理这个注解,实现MetaObjectHandler接口。有两种写法setFieldValByNamestrictInsertFill

3.0.5版本写法:

//把处理器加到IOC容器中!
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    //// 插入时的填充策略
    @Override
    public void insertFill(MetaObject metaObject) {
        log.error("insertFill start...........");
        this.setFieldValByName("gmtCreate", LocalDateTime.now(),metaObject);
        this.setFieldValByName("gmtModified", LocalDateTime.now(),metaObject);
        log.error("insertFill end...........");
    }

    // 更新时的填充策略
    @Override
    public void updateFill(MetaObject metaObject) {
        log.error("updateFill start...........");
        this.setFieldValByName("gmtModified", LocalDateTime.now(),metaObject);
        log.error("updateFill end...........");
    }
}

3.3.0之后版本3.4.2,当然前面的版本也兼容:

@Slf4j
@Component
public class MyNewMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        log.error("insertFill start...........");
        this.strictInsertFill(metaObject, "gmtCreate", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
        // 或者
        this.strictUpdateFill(metaObject, "gmtModified", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)

        log.error("insertFill end...........");
 }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.error("insertFill start...........");
        this.strictUpdateFill(metaObject, "gmtModified", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)

        log.error("insertFill end...........");
    }
}

乐观锁

乐观锁 : 故名思意十分乐观,它总是认为不会出现问题,无论干什么不去上锁!如果出现了问题,
再次更新值测试
悲观锁:故名思意十分悲观,它总是认为总是出现问题,无论干什么都会上锁!再去操作!

乐观锁实现方式:
取出记录时,获取当前 version
更新时,带上这个version
执行更新时, set version = newVersion where version = oldVersion
如果version不对,就更新失败

--乐观锁:1、先查询,获得版本号 version = 1
update user set name = "kuangshen", version = version + 1
where id = 2 and version = 1

测试Mybatis-plus的乐观锁插件实现

1、数据库表加version字段。

  `version` int(5) DEFAULT '1' COMMENT '版本号',

2、实体类加对应的字段

//乐观锁Version注解
@Version
private Integer version;

3、注册组件

//启动类上去扫描我们的dao包下的所有接口,我们可以把@MapperScan放在我们的MP配置类上也行
@MapperScan("com.wayne.dao")
@Configuration
@EnableTransactionManagement
public class MyBatisPlusConfig {

    // 注册乐观锁插件
    @Bean
    public OptimisticLockerInterceptor getOptimisticLockerInterceptor (){
        return new OptimisticLockerInterceptor();
    }

// 注册乐观锁插件
/*    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mbp = new MybatisPlusInterceptor();
        mbp.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return mbp;
    }*/
}

4、测试:

   @Test
    public void testOptimistic() {
        User user = userDao.selectById(9L);
        user.setName("大青山GGFG");
        user.setAge(21);
        user.setEmail("daqingshan@qq.com");
        int result = userDao.updateById(user);
        System.out.println(result);//1 ,受影响的行数
        System.out.println(user);//注意更新后的值也会回填到user对象中 User(id=9, name=大青山GREEN, age=210, email=daqingshan@qq.com, version=null, gmtCreate=null, gmtModified=null, deleteFlag=null)
    }

    @Test
    public void testOptimisticFail() {
        User user = userDao.selectById(9L);
        user.setName("大青山GGFG");
        user.setAge(21);
        user.setEmail("daqingshan@qq.com");
        // 模拟另外一个线程执行了插队操作
        User user2 = userDao.selectById(9L);
        user2.setName("大青山wwwwwww");
        user2.setEmail("daqingshan@163.com");
        userDao.updateById(user2);
        //注意这里并不会报错,而是会返回执行影响行数为0,表示没有update成功。因为下面这个语句的version值改变了所以更新失败
        //PDATE user SET name=?, age=?, email=?, version=?, gmt_modified=? WHERE id=? AND version=?
        int result = userDao.updateById(user);
        System.out.println(result);//1 ,受影响的行数
        //自旋锁来多次尝试提交!重新查一遍才能得到最新的version
    /*    while (0 == userDao.updateById(user)) {
            user = userDao.selectById(9L);
            user.setName("大青山GGFG");
            user.setAge(21);
            user.setEmail("daqingshan@qq.com");
        }*/
        System.out.println(user);//注意更新后的值也会回填到user对象中 User(id=9, name=大青山GREEN, age=210, email=daqingshan@qq.com, version=null, gmtCreate=null, gmtModified=null, deleteFlag=null)
    }

//待补充

疑问:

1、在引入maven依赖的时候为什么有些组件依赖可以不写version版本号就能依赖成功,而有些却不行呢?

如下面的lombok和mybatis-plus-boot-starter

<!--		lombok依赖-->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
<!--		mybatis-plus依赖-->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.0.5</version>
		</dependency>

这是因为maven继承的原因,父模块集成了公共的maven依赖,只需要依赖GA即可。

2、1、MyBatis-Plus如何帮我们把SQL写好了,我们的UserDao接口在实际运行中是如何创建实现类来具体实现我们想要的方法的?

参考

MyBatis-Plus官网

MyBatis-Plus教程——狂神

MyBatis、Spring、SpringMVC

基于3.0.5及3.4.2版本

扩展

Q: UTC和GMT的区别?

A:UTC是我们现在用的时间标准,GMT是老的时间计量标准。UTC是根据原子钟来计算时间,而GMT是根据地球的自转和公转来计算时间,也就是太阳每天经过位于英国伦敦郊区的皇家格林威治天文台的时间就是中午12点,。由于现在世界上最精确的原子钟50亿年才会误差1秒(最精确原子钟问世:50亿年误差一秒),可以说非常精确。而GMT因为是根据地球的转动来计算时间的,而地球的自转正在缓速变慢,所以使用GMT的话,总有一天,打个比方,中午12点,并不是一天太阳当头照的时候,很可能就是早上或者晚上了。所以说UTC更加精确。

[参考]( UTC和GMT什么关系? - 知乎用户的回答 - 知乎 https://www.zhihu.com/question/27052407/answer/56923233 )