Mybatis-Plus
概述
- 学mybatis-plus之前,要学会Mybatis、Spring、SpringMVC
- 已经有Mybatis了,为什么还要学习Mybatis-Plus呢?
- Mybatis-Plus可以节省我们大量工作时间,所有的CRUD代码它都可以自动化完成!即 偷懒
- 官网:
https://baomidou.com/
Mybatis本来简化jdbc的,Mybatis-Plus就是简化Mybatis的
类似于:JPA(Java持久层API)、tk-mapper、MybatisPlus
MyBatis-Plus(简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
特性
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
- 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作,BaseMapper
- 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求,以后简单的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 操作智能分析阻断,也可自定义拦截规则,预防误操作
1、快速入门
官方地址:https://baomidou.com/pages/226c21/#初始化工程
使用第三方组件:
- 导入对应的依赖
- 研究依赖如何配置
- 代码如何编写
- 提高扩展技术能力
步骤
- 创建数据库
mybatis_plus
- 创建表
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 '邮箱',
PRIMARY KEY (id)
);
- 给表里添加数据
DELETE FROM user;
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');
真实开发中,version(乐观锁)、deleted(逻辑删除)、create_time、update_time
- 编写项目,初始化项目
- 导入相关依赖
<!--数据库驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis-plus -->
<!-- mybatis-plus 是自己开发的,并非官方的 -->
<!--尽量不要同时导入mybatis和mybatis-plus,版本有差异-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.7.6</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
- 连接数据库
spring:
datasource:
url: jdbc:mysql://localhost:3306/数据库名?serverTimezone=GMT%2B8&useSSL=false&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
- 传统方式,pojo-dao(连接mybatis、配置mapper.xml文件)-service-controller
- mybatis-plus
- 编写pojo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private long id;
private String name;
private Integer age;
private String email;
}
- 编写mapper接口
// 如果我们在这没有@Mapper注解,我们就需要在主启动类里面加上@MapperScan("com.k2.dao"):包扫描 mapper接口的位置
@Mapper // 表名这是一个mapper接口
public interface UserMapper extends BaseMapper<User> {
// Mapper 继承该接口后,无需编写 mapper.xml 文件,即可获得CRUD功能
// 我们在这里继承BaseMapper、基础的CRUD,mybatis-plus已经帮我们写好了,直接使用即可
// mybatis里面,写完接口,还需要写mapper.xml,写sql语句
}
- 测试使用
@SpringBootTest
class MybatisPlus01HelloApplicationTests {
// @Autowored:默认是按照类型装配注入的
// @Resource:默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。
// 继承了BaseMapper,所有的方法都来自父类
@Autowored
UserMapper userMapper;
@Test
void contextLoads() {
// UserMapper继承了BaseMapper,所以我们使用了mybatis-plus,我们就可以直接调用基础的CRUD使用
// 参数是Wrapper,条件构造器
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
}
}
- 测试结果
2、配置日志
我们所有的sql语句都是不可见的,我们想知道它是怎么执行的(步骤),我们需要配置日志,让它输出出来
# 配置日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
- 这里也可以使用slf4j,log4j但是需要导入依赖包,我们在这里使用默认的
- OK!我们控制台已经输出出来,我们就可以注意这个自动生成的sql
3、CRUD扩展
3.1、insert
@Test
public void insertUser(){
User user = new User();
user.setName("koen");
user.setAge(18);
user.setEmail("1034174907@qq.com");
userMapper.insert(user);
System.out.println(user);
}
- 测试结果
数据库插入的id默认值:全局的唯一id
雪花算法(Twitter的snowflake):ID_WORKER
snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数(时间戳),10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0(占位符)
3.2、delete
// 根据id删除记录
@Test
public void testDeleteById(){
userMapper.deleteById(1597600799158972417L);
}
// 根据id批量删除
@Test
public void testDeleteBatchIds(){
userMapper.deleteBatchIds(Arrays.asList(1597608667711131651L,1597608667711131653L,1597608667711131652L));
}
// 通过map删除
@Test
public void testDeleteByMap(){
HashMap<String, Object> map = new HashMap<>();
map.put("name","xiaokoen");
userMapper.deleteByMap(map);
}
3.3、update
@Test
public void updateUser(){
User user = new User();
// 通过条件,自动调节动态sql
user.setId(5L);
user.setName("xiaokoen");
// 调用的参数虽然是updateById,但是参数是一个对象
userMapper.updateById(user);
}
- 测试结果
通过条件,自动调节动态sql
3.4、select
1、基本查询操作
// 通过id查询用户
@Test
public void testSelectById(){
User user = userMapper.selectById(1L);
System.out.println(user);
}
// 查询批量id
@Test
public void testSelectBatchIds(){
List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
users.forEach(System.out::println);
}
// 多条件查询之一:使用map
@Test
public void testSelectByMap(){
HashMap<String, Object> map = new HashMap<>();
// 自定义查询
map.put("name", "xiaokoen");
map.put("age",16);
List<User> users = userMapper.selectByMap(map);
users.forEach(System.out::println);
}
2、分页查询
分页在网站上使用非常多
- 原始使用limit进行分页
- 使用第三方插件pageHelper
- MP其实也内置了分页插件!
官网:https://baomidou.com/pages/97710a/#paginationinnerinterceptor
2.1、属性介绍
属性名 | 类型 | 默认值 | 描述 |
---|---|---|---|
overflow | boolean | false | 溢出总页数后是否进行处理(默认不处理,参见 插件#continuePage 方法) |
maxLimit | Long | 单页分页条数限制(默认无限制,参见 插件#handlerLimit 方法) | |
dbType | DbType | 数据库类型(根据类型获取应使用的分页方言,参见 插件#findIDialect 方法) | |
dialect | IDialect | 方言实现类(参见 插件#findIDialect 方法) |
建议单一数据库类型的均设置 dbType
2.2、使用步骤
- 配置拦截器MybatisPlusInterceptor
@MapperScan("com.k2.mapper") // 扫描我们的mapper文件
@EnableTransactionManagement // 开启事务管理
@Configuration // 配置类
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 创建MybatisPlusInterceptor拦截器对象
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 注册分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
- 直接使用Page对象
// 测试分页查询
@Test
public void testPage(){
// 参数一:当前页
// 参数二:页面大小
IPage<User> page = new Page<>(2,5);
page.getPages(); // 获取总页数
page.getTotal(); // 获取数据总数
page.getCurrent(); // 当前第几页
page.getRecords(); // 获取分页后的数据
IPage selectPage = userMapper.selectPage(page,null);
page.getRecords().forEach(System.out::println);
}
- 带条件的分页查询
@Test
public void testSelectPageByName() {
// 参数(1):当前页 参数(2):每页显示行数
IPage<User> page = new Page<User>(1, 3);
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.like(User::getName,"张");
IPage<User> selectPage = userMapper.selectPage(page, lambdaQueryWrapper);
log.info("总条数:{}", selectPage.getTotal());
selectPage.getRecords().forEach(user -> {
log.info("user ==> {}", user);
});
}
4、条件构造器Wrapper
官网:https://baomidou.com/pages/10c804/
Wrapper
我们写一些复杂的sql就可以使用它来替代
警告:
不支持以及不赞成在 RPC 调用中把 Wrapper 进行传输
- wrapper 很重
- 传输 wrapper 可以类比为你的 controller 用 map 接收值(开发一时爽,维护火葬场)
- 正确的 RPC 调用姿势是写一个 DTO 进行传输,被调用方再根据 DTO 执行相应的操作
- 我们拒绝接受任何关于 RPC 传输 Wrapper 报错相关的 issue 甚至 pr
为什么要RPC,RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
我们在这里测试几个:注意观察sql语句
- 查询name不为空的用户,并且邮箱不为空的用户,年龄大于等于15岁
@Test
public void test1() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper
.isNotNull("name")
.isNotNull("email")
.ge("age", 15);
userMapper.selectList(wrapper);
}
2. 查询名字koen(selectOne),出现多个相同名字,不要使用selectOne
@Test
public void test2() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "koen");
userMapper.selectOne(wrapper);
}
5、DQL条件查询
官网:https://baomidou.com/pages/10c804/
5.1、常规格式
- 条件查询1:常规格式(硬编码),age > 10 and age < 20
@Test
public void testFind01() {
QueryWrapper<User> queryWrapper = new QueryWrapper<User>();
queryWrapper.gt("age", 10);
queryWrapper.lt("age", 20);
// queryWrapper.gt("age",10).lt("age",20); 链式编程
List<User> list = userMapper.selectList(queryWrapper);
list.forEach(user -> {
log.info("【user】 == >{}", user);
});
}
5.2、Lambda格式
- 条件查询2:lambda方式,age > 20 and age < 30
@Test
public void testFind02() {
QueryWrapper<User> queryWrapper = new QueryWrapper<User>();
queryWrapper.lambda().gt(User::getAge, 20).lt(User::getAge, 30);
List<User> list = userMapper.selectList(queryWrapper);
list.forEach(user -> {
log.info("【user】 == >{}", user);
});
}
- 条件查询3:lambda方式(lambdaQueryWrapper),age > 10 and age < 20
@Test
public void testFind03() {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
lambdaQueryWrapper.gt(User::getAge, 10).lt(User::getAge, 20);
List<User> userList = userMapper.selectList(lambdaQueryWrapper);
userList.forEach(user -> log.info("【user】 ==>{}", user));
}
- 条件查询4(组合条件查询):or:或
@Test
public void testFind04() {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
lambdaQueryWrapper.gt(User::getAge, 10).or().lt(User::getAge, 20);
List<User> userList = userMapper.selectList(lambdaQueryWrapper);
userList.forEach(user -> log.info("【user】 ==>{}", user));
}
- 查询投影:只返回表中的特定字段
@Test
public void testFind05() {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
// select name,password from user
lambdaQueryWrapper.select(User::getName, User::getPassword);
List<User> userList = userMapper.selectList(lambdaQueryWrapper);
userList.forEach(user -> log.info("【user】 ==>{}", user));
}
- 查询投影:查询结果包含模型类中未定义的属性
@Test
public void testFind06() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// select count(*) as count from user
queryWrapper.select("count(*) as count");
List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
log.info("【user】 ==>{}", maps);
}
- 范围查询( > 、 = 、 between)
@Test
public void testFind07() {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<User>();
// 大于:>
// queryWrapper.gt(User::getAge, 20);
// 介于...之间:between
queryWrapper.between(User::getAge, 18, 30);
List<User> list = userMapper.selectList(queryWrapper);
list.forEach(user -> {
log.info("【user】 == >{}", user);
});
}
- 模糊查询(like)
@Test
public void testFind08() {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
lambdaQueryWrapper.like(User::getName, "k");
List<User> userList = userMapper.selectList(lambdaQueryWrapper);
userList.forEach(user -> log.info("【user】 ==>{}", user));
}
lambdaQueryWrapper.likeLeft(User::getName, "k"); // %在左边
lambdaQueryWrapper.likeRight(User::getName, "k"); // %在右边
- 包涵性匹配(in)
@Test
public void testFind09() {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
// lambdaQueryWrapper.in(User::getAge, 8, 12, 16);
lambdaQueryWrapper.in(User::getAge, Arrays.asList(8, 12, 16));
List<User> users = userMapper.selectList(lambdaQueryWrapper);
users.forEach(user -> log.info("【user】 ==>{}", user));
}
- 内嵌子查询(id在子查询中查出来)
@Test
public void test5() {
QueryWrapper<User> queryWrapper = new QueryWrapper<User>();
queryWrapper.inSql("id", "select id from user where id < 3");
List<User> users = userMapper.selectList(lambdaQueryWrapper);
users.forEach(user -> log.info("【user】 ==>{}", user));
}
- 分组查询(group by)
@Test
public void testFind10() {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.select(User::getAge);
lambdaQueryWrapper.groupBy(User::getAge);
List<User> userList = userMapper.selectList(lambdaQueryWrapper);
userList.forEach(user -> log.info("【user】 ==>{}", user));
}
- 排序查询(order by)
@Test
public void testFind11() {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
// 参数1:是否排序 参数2:true:正序/false:倒叙 参数3:按照什么排序
lambdaQueryWrapper.orderBy(true, false, User::getAge);
List<User> users = userMapper.selectList(lambdaQueryWrapper);
users.forEach(user -> {
log.info("【user】 ==>{}", user);
});
}
- orderByAsc:正序
@Test
public void testFind12() {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
// 参数1:是否正序排序 参数2:按照什么排序
lambdaQueryWrapper.orderByAsc(true, User::getAge);
List<User> users = userMapper.selectList(lambdaQueryWrapper);
users.forEach(user -> {
log.info("【user】 ==>{}", user);
});
}
- orderByDesc:倒叙
@Test
public void testFind13() {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>();
// 参数1:是否倒叙排序 参数2:按照什么排序
lambdaQueryWrapper.orderByDesc(true, User::getAge);
List<User> users = userMapper.selectList(lambdaQueryWrapper);
users.forEach(user -> {
log.info("【user】 ==>{}", user);
});
}
6、实际开发中常遇到的问题
6.1、表字段与编码属性设计不同步
- 属性注解:@TableField
- 位置:定义在属性上
- 作用:设置当前属性对应的数据库表中的字段关系
- 相关属性
- value(默认):设置数据库表字段名称
6.2、编码中添加了数据库中未定义的属性
- 属性注解:@TableField
- 位置:定义在属性上
- 作用:设置当前属性对应的数据库表中的字段关系
- 相关属性
- value:设置数据库表字段名称
- exist:设置属性在数据库表字段中是否存在,默认为true,此属性无法与value合并使用
6.3、采用默认查询开放了更多的字段查看权限
我们查询用户表的时候,一般不会查询 密码 字段
- 属性注解:@TableField
- 位置:定义在属性上
- 作用:设置当前属性对应的数据库表中的字段关系
- 相关属性
- value:设置数据库表字段名称
- exist:设置属性在数据库表字段中是否存在,默认为true,此属性无法与value合并使用
- select:设置属性是否参与查询,默认是true,此属性与select()映射配置不冲突
6.4、表名与编码开发设计不同步
- 类注解:@TableName
- 位置:定义在类上方
- 作用:设置当前类对应于数据库表关系
- 相关属性
- value:设置数据库表名称
7、DML编程控制
7.1、主键生成策略(Insert)
分布式系统唯一id生成:https://www.cnblogs.com/haoxinyue/p/5208136.html
- AUTO(0):使用数据库id自增策略控制id生成
- NONE(1):不设置id生成策略
- INPUT(2):用户手工输入id
- ASSIGN_ID(3):雪花算法生成id(可兼容数值型与字符串型)
- ASSIGN_UUID(4):以UUID生成算法作为id生成策略(对数据库性能有影响)
主键源码解析
- 属性注解:@TableId
- 位置:定义在属性上方
- 作用:设置当前类中主键属性的生成策略
- 相关属性
- value:设置数据库主键名称
- type:设置主键属性的生成策略,值参照IdType枚举值
主键自增:AUTO(0)
我们需要配置主键自增:
- 实体类字段上 @TableId(type = IdType.AUTO):一定要写,否则还是默认的
- 数据库字段要设置成自增!
默认 ASSIGN_ID:全局唯一ID
雪花算法(Twitter的snowflake):ID_WORKER
snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数(时间戳),10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0(占位符)
7.2、主键策略全局配置
mybatis-plus:
global-config:
db-config:
id-type: auto
table-prefix: ta1_
7.3、逻辑删除(Delete)
官网:https://baomidou.com/pages/6b03c5/
说明:
只对自动注入的 sql 起效:
- 插入: 不作限制
- 查找: 追加 where 条件过滤掉已删除数据,如果使用 wrapper.entity 生成的 where 条件也会自动追加该字段
- 更新: 追加 where 条件防止更新到已删除数据,如果使用 wrapper.entity 生成的 where 条件也会自动追加该字段
- 删除: 转变为 更新
物理删除:在数据库中直接移除
逻辑删除:在数据库中没有被移除,而是通过一个变量来让他失效!
由deleted=0 ==> deleted=1
管理员可以查看被删除的记录!防止数据丢失,类似于回收站
1、代码测试
- 在数据表中增加一个deleted字段
- 同步实体类
@TableLogic // 逻辑删除
private Integer deleted;
- 配置yaml
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
- 高版本不用手动配置,只需要添加注解就行 @TableLogic
- 测试结果
- 测试后变为:记录依旧还在,只是值已经变化
- 它执行的并不是删除操作,只是一个更新操作
- 我们再查询id=1的用户发现已经查询不到了:查询的时候自动过滤
7.4、自动填充(create_time、update_time)
官网:https://baomidou.com/pages/4c6bcf/
-
创建时间、修改时间,这些操作一般都是自动化完成的,我们不希望手动更新!
-
所有的数据库表:gmt_create、gmt_modified几乎所有的表都要配置上!而且需要自动化
1、方式一:数据库级别
工作中不可以修改数据库(牢记)
- 在表中添加字段:create_time、update_time
默认是以时间戳方式
- 同步实体类
private LocalDateTime createTime;
private LocalDateTime updateTime;
- 测试结果
再次运行修改操作,虽然我们没有修改时间,但是他们会自动帮我们修改
2、方式二:代码级别(常用)
- 删除数据库的默认值
- 实体类字段属性上需要增加注解(可以看源码)
// 字段自动填充策略
@TableField(fill = FieldFill.INSERT) // 插入时更新字段
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入和修改时更新字段
private Date updateTime;
- 创建处理器处理注解
@Slf4j
@Component // 不要忘记把处理器加到IOC容器中
public class MyMetaObjectHandler implements MetaObjectHandler {
// 插入时的填充策略
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill-----");
LocalDateTime now = LocalDateTime.now();
// 方式一
this.strictInsertFill(metaObject,"createTime", LocalDateTime.class,now);
this.strictInsertFill(metaObject,"updateTime",LocalDateTime.class,now);
// 方式二
// 添加时间
metaObject.setValue("createTime", now);
// 修改时间
metaObject.setValue("updateTime", now);
}
// 更新时的填充策略
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill-----");
LocalDateTime now = LocalDateTime.now();
this.strictInsertFill(metaObject,"updateTime",LocalDateTime.class,now);
// 修改时间
metaObject.setValue("updateTime", now);
}
}
- 测试结果
7.5、乐观锁(Update)
官网:https://baomidou.com/pages/0d93c0/
面试过程中,经常会被问到乐观锁、悲观锁
乐观锁:故名思意十分乐观,它总会认为不会出现问题,无论干什么都不会上锁,如果出现问题,再次更新测试
悲观锁:故名思意十分悲观,它总会认为会出现问题,无论干什么都会上锁,再操作
当要更新一条记录的时候,希望这条记录没有被别人更新
乐观锁实现方式:
- 增加版本号,数据在提交前,进行版本检测,如果提交时和取出数据时的版本一致,才可以提交。
- 取出记录时,获取当前 version
- 更新时,带上这个 version
- 执行更新时, set version = newVersion where version = oldVersion
- 如果 version 不对,就更新失败
测试MP的乐观锁插件
- 给数据库增加version字段,默认为1
- 同步实体类
@Version // 乐观锁的注解
private Integer version;
- 配置拦截器(注册组件)
@MapperScan("com.k2.mapper") // 扫描我们的mapper文件
@EnableTransactionManagement // 开启事务管理
@Configuration // 配置类
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 创建MybatisPlusInterceptor拦截器对象
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 注册乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
- 模拟乐观锁成功状态
// 模拟乐观锁成功状态
@Test
public void testLock(){
User user = userMapper.selectById(1L);
user.setName("koen小");
user.setEmail("k2study@163.com");
userMapper.updateById(user);
}
- 模拟乐观锁失败状态
// 模拟乐观锁失败状态
@Test
public void testLock2(){
// 线程1
User user = userMapper.selectById(1L); // version1 = oldVersion
user.setName("koen小");
user.setEmail("k2study@163.com");
// 模拟线程2 突然来了,线程1 user获取到了,但是还没有修改
User user2 = userMapper.selectById(1L); // version2 = oldVersion
user2.setName("koen小");
user2.setEmail("k2study@163.com");
userMapper.updateById(user2); // version2 = newVersion
// 自旋锁来多次尝试提交(JUC里面学)
// 如果没有乐观锁就会覆盖插队线程的值
userMapper.updateById(user); // version1 = newVersion
}
自旋锁来多次尝试提交(JUC里面学)
- 测试结果
8、三层封装
8.1、Mapper封装
Mybatis-Plus 为了开发更加快捷,对持久层也进行了封装,直接提供了相关的接口。我们在进行持久层开发时,可以继承它提供的接口,使得编码更加高效
// 如果我们在这没有@Mapper注解,我们就需要在主启动类里面加上@MapperScan("com.k2.dao"):包扫描 mapper接口的位置
@Mapper // 表名这是一个mapper接口
public interface UserMapper extends BaseMapper<User> {
// Mapper 继承该接口后,无需编写 mapper.xml 文件,即可获得CRUD功能
// 我们在这里继承BaseMapper、基础的CRUD,mybatis-plus已经帮我们写好了,直接使用即可
// mybatis里面,写完接口,还需要写mapper.xml,写sql语句
}
8.2、Service封装
Mybatis-Plus 为了开发更加快捷,对业务层也进行了封装,直接提供了相关的接口和实现类。我们在进行业务层
开发时,可以继承它提供的接口和实现类,使得编码更加高效
-
定义Service接口,并继承Iservice
-
定义实现类,并继承ServiceImpl<Mapper,>之后,再实现定义的接口(注意:此处使用的泛型即是我们的Entity实体类)
public interface UserService extends IService<User>{}
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService{}
9、性能分析插件(执行SQL分析打印)
- 导入依赖
<!-- 导入数据库驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- mybatis-plus -->
<!-- mybatis-plus 是自己开发的,并非官方的 -->
<!--尽量不要同时导入mybatis和mybatis-plus,版本有差异-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- 执行SQL分析打印 -->
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.9.1</version>
</dependency>
- 修改yaml配置文件
我们要注意,在这主要修改的是:driver-class-name 和 url
spring:
# 数据库连接配置
datasource:
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
url: jdbc:p6spy:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&useSSL=false&useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
profiles:
active: dev
# 原始的
# url: jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&useSSL=false&useUnicode=true&characterEncoding=utf-8
# driver-class-name: com.mysql.cj.jdbc.Driver
# username: root
# password: 123456
- 添加spy.properties配置文件
# 3.2.1以上使用
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
# 3.2.1以下使用或者不配置
#modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
# 日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
- 查询结果
但是也是非常耗时的,不建议使用
10、代码生成器
官网:https://baomidou.com/pages/981406
自动生成:dao、pojo、service、controller、mapper映射
10.1、方式一:3.5.1版本及其以上版本
- 导入依赖,整合mybatis-plus的jar
<!--添加 模板引擎 依赖,MyBatis-Plus 支持 Velocity(默认):代码生成器需要-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<!--代码生成器 3.5.1版本及其以上版本-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3</version>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!--mp依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
<!-- 执行SQL分析打印(数据源) -->
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.9.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-common</artifactId>
<version>3.0.0</version>
</dependency>
- 改服务端口、运行环境 、连接数据库
spring:
# 数据库连接配置
datasource:
url: jdbc:mysql://localhost:3306/数据库名?serverTimezone=GMT%2B8&useSSL=false&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
# driver-class-name: com.p6spy.engine.spy.P6SpyDriver
# url: jdbc:p6spy:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&useSSL=false&useUnicode=true&characterEncoding=utf-8
# username: root
# password: 123456
profiles:
active: dev
# 配置日志
mybatis-plus:
mapper-locations: classpath:mybatis/mapper/*.xml
configuration: # 指定mybatis全局配置文件中相关配置(就是mybatis-config.xml)
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启默认日志输出
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
# 服务端口(可以按自己的配置)
server:
port: 9000
- 创建自动生成器类
package com.k2;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.fill.Column;
import java.util.Collections;
// 代码生成器
public class KoenCode {
private final static String URL = "jdbc:mysql://localhost:3306/数据库名?serverTimezone=GMT%2B8&useSSL=false&useUnicode=true&characterEncoding=utf-8";
private final static String USERNAME = "root";
private final static String PASSWORD = "123456";
public static void main(String[] args) {
String projectPath = System.getProperty("user.dir");
System.out.println(projectPath);
FastAutoGenerator.create(URL, USERNAME, PASSWORD)
// 全局配置
.globalConfig(builder -> {
builder.author("xiaokoen") // 设置作者
.outputDir(projectPath + "/src/main/java/") // 指定输出目录
.enableSwagger() // 开启 swagger2 模式
.fileOverride() // 覆盖已生成文件(默认为false)
.disableOpenDir() // 禁止打开输出目录,默认为true
.dateType(DateType.ONLY_DATE) // 时间策略(默认DateType.TIME_PACK)
.commentDate("yyyy-MM-dd") // 注释日期(默认值: yyyy-MM-dd)
;
})
// 包配置
.packageConfig(builder -> {
builder.parent("com.k2") // 设置父包名
// .moduleName("blog") // 设置父包模块名
.entity("pojo")
.mapper("mapper")
.service("service")
// .serviceImpl("service.impl") // 默认
.controller("controller")
// .xml("mapper.xml") // 默认
.pathInfo(Collections.singletonMap(OutputFile.xml, projectPath + "/src/main/resources/mapper")); // 设置mapperXml生成路径
})
// 策略配置
.strategyConfig(builder -> {
builder.addInclude("t_user") // 设置需要生成的表名
.addTablePrefix("t_", "c_")// 设置过滤表前缀
// 实体类策略
.entityBuilder()
.enableLombok() // 开启lombok
.enableTableFieldAnnotation() // 开启生成实体时,生成字段注解
.naming(NamingStrategy.underline_to_camel) // 下划线转驼峰命(默认)
.columnNaming(NamingStrategy.underline_to_camel) // 下划线转驼峰命
// 乐观锁
.versionColumnName("version") // 乐观锁字段名(数据库)
.versionPropertyName("version") // 乐观锁属性名(实体)
// 逻辑删除
.logicDeleteColumnName("deleted") // 逻辑删除字段名(数据库)
.logicDeletePropertyName("deleted") // 逻辑删除属性名(实体)
// 自动填充配置
.addTableFills(new Column("create_time", FieldFill.INSERT), new Column("update_time", FieldFill.INSERT_UPDATE))
// .addTableFills(new Property("create_time",FieldFill.INSERT),new Property("update_time",FieldFill.INSERT_UPDATE))
// .formatFileName("%sEntity") // 格式化文件名称
.idType(IdType.AUTO) // 全局主键类型
// service 策略
.serviceBuilder()
.formatServiceFileName("%sService")
.formatServiceImplFileName("%sServiceImpl")
// controller 策略
.controllerBuilder()
.enableHyphenStyle() // 开启驼峰转连字符
.enableRestStyle() // 开启生成@RestController 控制器
;
})
// .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
- 测试结果
- 运行过程中,遇到版本不一致问题
- 解决方案:我们只需要重新build项目即可(两个都可以试试)
10.2、方式二:3.5.1以下版本
- 导入依赖,整合mybatis-plus的jar
<!--代码生成器3.5.1版本以下-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!--添加 模板引擎 依赖,MyBatis-Plus 支持 Velocity(默认):代码生成器需要-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!--mp依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
- 改服务端口、运行环境 、连接数据库
spring:
# 数据库连接配置
datasource:
url: jdbc:mysql://localhost:3306/数据库名?serverTimezone=GMT%2B8&useSSL=false&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
# driver-class-name: com.p6spy.engine.spy.P6SpyDriver
# url: jdbc:p6spy:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&useSSL=false&useUnicode=true&characterEncoding=utf-8
# username: root
# password: 123456
profiles:
active: dev
# 配置日志
mybatis-plus:
mapper-locations: classpath:mybatis/mapper/*.xml
configuration: # 指定mybatis全局配置文件中相关配置(就是mybatis-config.xml)
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启默认日志输出
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
# 服务端口(可以按自己的配置)
server:
port: 9000
- 创建自动生成器类
public class GeneratorUtils {
private final static String DB_URL = "jdbc:mysql://localhost/mybatisplus_db?serverTimezone=UTC";
private final static String USER_NAME = "root";
private final static String PASSWORD = "123456";
private final static String AUTHOR = "koen";
private final static String DRIVE = "com.mysql.cj.jdbc.Driver";
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java/test");
gc.setAuthor(AUTHOR);
gc.setOpen(false);
gc.setControllerName("%sController");
gc.setServiceName("I%sService");
gc.setServiceImplName("%sServiceImpl");
gc.setMapperName("%sMapper");
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
// dsc.setSchemaName("public");
dsc.setUrl(DB_URL);
dsc.setDriverName(DRIVE);
dsc.setUsername(USER_NAME);
dsc.setPassword(PASSWORD);
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.koen");
pc.setEntity("pojo");
pc.setMapper("mapper");
pc.setController("controller");
pc.setService("service");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 velocity
String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
// 要生成的表
strategy.setInclude("tbl_user");
strategy.setTablePrefix("tbl_");
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
strategy.setLogicDeleteFieldName("deleted");
strategy.setVersionFieldName("version");
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
mpg.execute();
}
}