Mybatis-Plus官网:https://www.baomidou.com/
源码地址:gitee https://gitee.com/baomidou/mybatis-plus
github https://github.com/baomidou/mybatis-plus
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
任何能使用
MyBatis
进行 CRUD, 并且支持标准 SQL 的数据库,具体支持情况如下,如果不在下列表查看分页部分教程 PR 您的支持。
总的来说,Mybatis-Plus 大大提高了框架的开发效率,让开发人员能够更加快速、方便地实现业务开发。当然,Mybatis-Plus 也有它的缺点,例如对于复杂查询和多表查询的支持不够完善等。不过,随着 Mybatis-Plus 的不断发展,相信它在后续的版本中会不断完善和优化,成为更加优秀的 ORM 框架。
如果你是 Mybatis 的使用者,那么可以尝试使用Mybatis-Plus,这将大大简化你的代码开发流程,提高你的开发效率。
注解 | 用途 | 描述 |
---|---|---|
@TableName | 定义数据表名 | 为实体类定义数据库表名,可以设置驼峰转下划线等自动转换方式 |
@TableId | 定义主键ID | 定义主键字段,可以设置主键类型、主键生成策略、是否自增等 |
@TableField | 定义数据库列名 | 可以设置列名、是否为主键、是否为自动填充等 |
@Version | 数据版本控制 | 定义版本属性,实现乐观锁功能 |
@EnumValue | 枚举属性与数据库值的映射 | 用于确定枚举属性的值和相应在数据库中的值的映射关系 |
@TableLogic | 逻辑删除标识 | 定义逻辑删除属性,用于标识数据是否被删除 |
Mybatis-Plus提供了强大的条件构造器。这里简要介绍了条件构造器的结构与关系,并通过简要的示例描述查询、删除和修改操作的条件构造器实现方式。
Wrapper : 条件构造抽象类,最顶端父类
AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
QueryWrapper : Entity 对象封装操作类,不是用lambda语法
UpdateWrapper : Update 条件封装,用于Entity对象更新操作
AbstractLambdaWrapper : Lambda 语法使用 Wrapper统一处理解析 lambda 获取column。
LambdaQueryWrapper :看名称也能明白就是用于Lambda语法使用的查询Wrapper
LambdaUpdateWrapper : Lambda 更新封装Wrapper
方法 | 符号 | 示例 | 说明 |
---|---|---|---|
eq | = | eq("age", 20) |
等于 |
ne | != | ne("age", 20) |
不等于 |
gt | > | gt("age", 18) |
大于 |
ge | >= | ge("age", 18) |
大于等于 |
lt | < | lt("age", 25) |
小于 |
le | <= | le("age", 25) |
小于等于 |
between | BETWEEN … AND … | between("age", 18, 25) |
在指定范围之间 |
notBetween | NOT BETWEEN … AND … | notBetween("age", 18, 25) |
不在指定范围之间 |
like | LIKE | `like(“name”, “王”) | 模糊查询 |
notLike | NOT LIKE | notLike("name", "王") |
不模糊查询 |
in | IN | in("age", Arrays.asList(18, 20, 22)) |
包含某些值 |
notIn | NOT IN | notIn("age", Arrays.asList(18, 20, 22)) |
不包含某些值 |
isNull | IS NULL | isNull("name") |
为空 |
isNotNull | IS NOT NULL | isNotNull("name") |
不为空 |
orderByAsc | ASC | orderByAsc("age") |
升序排序 |
orderByDesc | DESC | orderByDesc("age") |
降序排序 |
配置项 | 描述 | 默认值 | 取值范围 |
---|---|---|---|
spring.datasource.url | 数据库连接字符串 | 无 | 数据库的连接字符串 |
spring.datasource.username | 数据库账号 | 无 | 数据库账号 |
spring.datasource.password | 数据库密码 | 无 | 数据库密码 |
spring.datasource.driver-class-name | 数据库驱动类名 | 无 | 数据库驱动类名 |
mybatis-plus.mapper-locations | Mybatis-Plus映射文件位置 | 无 | Mybatis-Plus映射文件所在位置 |
mybatis-plus.global-config.db-config.page-size | 每页显示的记录数 | 10 | 大于0的整数 |
mybatis-plus.global-config.db-config.page-helper | 是否开启分页插件 | true | true或false |
mybatis-plus.global-config.db-config.logic-delete-value | 逻辑删除时的值 | 1 | 自定义类型 |
mybatis-plus.global-config.db-config.logic-not-delete-value | 逻辑未删除时的值 | 0 | 自定义类型 |
mybatis-plus.global-config.db-config.id-type | 主键生成策略 | auto | 自增主键、UUID主键、雪花算法主键、全局唯一ID主键、用户自定义主键类型 |
mybatis-plus.global-config.db-config.field-strategy | 表字段处理策略 | ignore | include或exclude |
mybatis-plus.global-config.db-config.optimistic-lock-field | 乐观锁字段名 | 无 | 表的非主键字段名 |
mybatis-plus.db-config.table-prefix | 数据库表前缀 | 无 | 表前缀 |
mybatis-plus.db-config.capital-mode | 数据库是否大小写敏感 | true | true或false |
配置项 | 描述 | 默认值 | 取值范围 |
---|---|---|---|
map-underscore-to-camel-case | 是否开启字段下划线转驼峰 | false | true/false |
cache-enabled | 是否开启二级缓存 | false | true/false |
lazy-loading-enabled | 是否开启延迟加载 | false | true/false |
aggressive-lazy-loading | 是否开启懒加载,当开启时,会影响所有延迟加载设置 | false | true/false |
multiple-result-sets-enabled | 是否开启使用列标签代替列名 | true | true/false |
use-column-label | 是否允许一次返回多个结果集,需要驱动支持 | false | true/false |
use-generated-keys | 是否允许JDBC支持自动生成主键 | false | true/false |
default-statement-timeout | 设置超时时间,单位为秒,可以被单独的 statement 覆盖 | 无 | 整数 |
default-fetch-size | 设置 JDBC 数据库查询时每次返回的最大结果集条数,可以被单独的 statement 覆盖 | 无 | 整数 |
default-statement-type | 设置默认的statement类型,可选值为:STATEMENT, PREPARED, CALLABLE | PREPARED | STATEMENT/PREPARED/CALLABLE |
default-executor-type | 设置默认的 Executor 类型,可选值为:SIMPLE, REUSE, BATCH | SIMPLE | SIMPLE/REUSE/BATCH |
safe-row-bounds-enabled | 是否允许在嵌套语句中使用分页(RowBounds) | false | true/false |
use-aggressive-mappings | 是否启用行为null时的字段映射,即不为空时才映射该字段 | false | true/false |
call-setters-on-nulls | 当 null 值被手动设置到 SQL 参数时是否调用 Setter 方法 | false | true/false |
return-instance-for-empty-row | 当返回值为空时,是否返回 null 值,而不是一个新的实例对象 | false | true/false |
log-prefix | 日志前缀 | 无 | 字符串 |
configuration-factory | 返回自定义配置的 ConfigurationFactory | 无 | 字符串 |
log-impl | 日志mybatis-plus SQL 打印 | org.apache.ibatis.logging.stdout.StdOutImpl |
希望以上内容对您有所帮助,如何不足请谅解。
#mybatis-plus
mybatis-plus.mapper-locations=classpath*:**/mapper/xml/*.xml
mybatis-plus.type-aliases-package=com.caochenlei.mpdemo.pojo
mybatis-plus.type-handlers-package=com.caochenlei.mpdemo.type
mybatis-plus.type-enums-package=com.caochenlei.mpdemo.enum
mybatis-plus.check-config-location=false
mybatis-plus.executor-type=simple
#mybatis-plus.configuration
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.default-enum-type-handler=org.apache.ibatis.type.EnumTypeHandler
mybatis-plus.configuration.aggressive-lazy-loading=true
mybatis-plus.configuration.lazy-loading-enabled=true
mybatis-plus.configuration.auto-mapping-behavior=partial
mybatis-plus.configuration.auto-mapping-unknown-column-behavior=none
mybatis-plus.configuration.local-cache-scope=session
mybatis-plus.configuration.cache-enabled=true
mybatis-plus.configuration.call-setters-on-nulls=false
mybatis-plus.configuration.configuration-factory=
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#mybatis-plus.global-config
mybatis-plus.global-config.banner=true
mybatis-plus.global-config.enable-sql-runner=false
mybatis-plus.global-config.super-mapper-class=com.baomidou.mybatisplus.core.mapper.Mapper
#mybatis-plus.global-config.db-config
mybatis-plus.global-config.db-config.id-type=assign_id
mybatis-plus.global-config.db-config.table-prefix=tbl_
mybatis-plus.global-config.db-config.schema=
mybatis-plus.global-config.db-config.column-format=
mybatis-plus.global-config.db-config.property-format=
mybatis-plus.global-config.db-config.table-underline=true
mybatis-plus.global-config.db-config.capital-mode=false
mybatis-plus.global-config.db-config.logic-delete-field=
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0
mybatis-plus.global-config.db-config.insert-strategy=not_null
mybatis-plus.global-config.db-config.update-strategy=not_null
mybatis-plus.global-config.db-config.select-strategy=not_null
CRUD简单实现
1、根据我上一张的文章进行SpringBoot项目搭建 :https://blog.csdn.net/ITKidKid/article/details/130863898
# 该 SQL 命令用于删除名为 testdemo 的数据库。如果该数据库不存在,则不执行任何操作。它通常用于删除整个数据库,以便重新创建或者清空数据库。
DROP DATABASE IF EXISTS testdemo;
# 该 SQL 命令选择 testdemo 数据库,并把它设为当前正在使用的数据库,以便接下来的操作默认在该数据库中执行。
USE testdemo;
# 这是一个用于删除名为 "test_demo" 的数据库表的 SQL 命令。它先检查该表是否存在,如果存在就删除它。通常用于重新创建一个表。
DROP TABLE IF EXISTS test_demo;
# 创建表
CREATE TABLE test_demo (
id INT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
username VARCHAR(50) NOT NULL COMMENT '用户名',
password VARCHAR(50) NOT NULL COMMENT '密码',
email VARCHAR(50) NOT NULL COMMENT '邮箱',
mobile CHAR(11) NOT NULL COMMENT '手机号',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账号信息表';
# 新增数据
INSERT INTO test_demo (username, password, email, mobile, create_time, update_time) VALUES
('user1', 'password1', '[email protected]', '13888888888', NOW(), NOW()),
('user2', 'password2', '[email protected]', '13999999999', NOW(), NOW()),
('user3', 'password3', '[email protected]', '13111111111', NOW(), NOW()),
('user4', 'password4', '[email protected]', '13222222222', NOW(), NOW()),
('user5', 'password5', '[email protected]', '13333333333', NOW(), NOW());
生成相关的结构代码:https://blog.csdn.net/ITKidKid/article/details/126185295
这些操作执行完了,先把代码生成的依赖给去除,后续还需要使用的话也可以不用删除,
启动类添加注解 @MapperScan,@MapperScan是MyBatis框架中的一个注解,用于扫描Mapper接口的注解,将Mapper接口和SQL语句进行绑定。它的作用是在启动项目时自动扫描包中的所有Mapper接口,并生成对应的代理实现类,并将它们注册到Spring容器中,方便在其他组件中进行依赖注入。
@MapperScan("com.example.demo.mapper")
如果有就没必要重复添加了
org.springframework.boot
spring-boot-starter-web
com.baomidou
mybatis-plus-boot-starter
3.5.2
mysql
mysql-connector-java
8.0.29
org.projectlombok
lombok
1.18.24
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/testdemo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: 数据库账号
password: 数据库密码
hikari:
# 连接池名
pool-name: DataHikariCP
# 最小空闲连接数
minimum-idle: 5
# 空闲连接存活最大时间,默认60000(10分钟)
idle-timeout: 180000
# 最大连接数,默认10
maximum-pool-size: 10
# 从连接池返回的连接的自动提交
auto-commit: true
# 连接最大存活时间,0表示永久存活,默认1800000(30分钟)
max-lifetime: 1800000
# 连接超时时间,默认30000(30秒)
connection-timeout: 30000
# 测试连接是否可用的查询语句
connection-init-sql: SELECT 1
# mybatis-plus配置
mybatis-plus:
configuration:
# 是否关闭驼峰转换
map-underscore-to-camel-case: true
auto-mapping-behavior: full
# 日志mybatis-plus SQL 打印
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
普通增删改查实现
/**
*
* 前端控制器
*
*
* @author zzw
* @since 2023-05-26
*/
@RestController
@RequestMapping("/testDemo")
public class TestDemoController {
@Resource
private TestDemoService testDemoService;
@GetMapping("/list")
Map<String,Object> list() {
HashMap<String,Object> map = new HashMap<>();
List<TestDemo> list = testDemoService.list(null); // 无查询条件
LambdaQueryWrapper<TestDemo> queryWrapper = new LambdaQueryWrapper<>();
// queryWrapper.eq(条件,参数,参数值);
queryWrapper.between(1==1, TestDemo::getCreateTime,"2023-05-31 00:00:00","2023-05-31 23:59:59")
.eq(1!=1, TestDemo::getUsername,"user1")
.like(1==1, TestDemo::getMobile,"133");
List<TestDemo> list1 = testDemoService.list(queryWrapper); // 使用条件构造器查询
map.put("code",200);
map.put("message","查询成功");
map.put("list",list);
map.put("list1",list1);
return map;
}
@DeleteMapping("/delete")
Map<String,Object> delete(@RequestParam Integer id) {
HashMap<String,Object> map = new HashMap<>();
boolean remove = testDemoService.removeById(id);
if (remove){
map.put("code",200);
map.put("message","删除成功");
}else {
map.put("code",500);
map.put("message","删除失败");
}
return map;
}
@PutMapping(value = "/update")
Map<String,Object> update(@RequestBody TestDemo testDemo) {
HashMap<String,Object> map = new HashMap<>();
boolean update = testDemoService.updateById(testDemo);
if (update){
map.put("code",200);
map.put("message","修改成功");
}else {
map.put("code",500);
map.put("message","修改失败");
}
return map;
}
@PostMapping("/insert")
Map<String,Object> insert() {
HashMap<String,Object> map = new HashMap<>();
TestDemo testDemo = new TestDemo();
testDemo.setCreateTime(LocalDateTime.now());
testDemo.setUsername("admin1");
testDemo.setMobile("15698756234");
testDemo.setPassword("123456");
testDemo.setEmail("[email protected]");
boolean save = testDemoService.save(testDemo);
if (save){
map.put("code",200);
map.put("message","新增成功");
}else {
map.put("code",500);
map.put("message","新增失败");
}
return map;
}
}
注意: 所有的配置类编写在 com.example.demo.config 包下面
配置类编写
@Configuration
public class MybatisPlusConfig {
/**
* 分页配置
* @return 3.3.1及以下版本使用下面这个分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
/**
* 分页配置
* @return 3.3.1以上版本使用下面这个分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //插件分页拦截器,我的是mysql
return mybatisPlusInterceptor;
}
}
代码实战
/**
*
* @param currPageNo 当前页码
* @param pageSize 每页条数
* @return
*/
@PostMapping("/page")
Map<String,Object> page(Integer currPageNo,Integer pageSize) {
HashMap<String,Object> map = new HashMap<>();
Page<TestDemo> page = new Page<>(currPageNo, pageSize);
Page<TestDemo> page1 = testDemoService.page(page);
Integer total = Math.toIntExact(page1.getTotal());
Integer current = Math.toIntExact(page1.getCurrent());
map.put("total",total); // 总条数
map.put("current",current); // 当前页码
map.put("content",page1.getRecords()); // 查询出来的数据
map.put("code",200);
map.put("message","查看分页数据成功");
return map;
}
插件功能:防止全表更新与全表删除,依次保护数据库的安全,建议开发环境使用,不建议生产环境使用。
添加插件:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //插件分页拦截器,我的是mysql
mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 添加执行分析插件
return mybatisPlusInterceptor;
}
}
代码实战:
@DeleteMapping("/delete")
Map delete() {
HashMap map = new HashMap<>();
boolean remove = testDemoService.remove(null);
if (remove){
map.put("code",200);
map.put("message","删除成功");
}else {
map.put("code",500);
map.put("message","删除失败");
}
return map;
}
结果:
报错:com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of full table deletion(禁止删除全表)
插件功能:该功能依赖 p6spy
组件,完美的输出打印 SQL 及执行时长, 3.1.0
以上版本支持。
添加依赖:
<dependency>
<groupId>p6spygroupId>
<artifactId>p6spyartifactId>
<version>3.9.1version>
dependency>
修改配置:application.yml
spring:
datasource:
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
url: jdbc:p6spy:mysql://localhost:3306/testdemo
username: # 数据库账号
password: # 数据库密码
添加配置:spy.properties
# 3.2.1以上使用
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,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=com.mysql.jdbc.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
注意问题:
driver-class-name 为 p6spy 提供的驱动类
url 前缀为 jdbc:p6spy 跟着冒号为对应数据库连接地址
打印出sql为null,在excludecategories增加commit
批量操作不打印sql,去除excludecategories中的batch
批量操作打印重复的问题请使用MybatisPlusLogFactory (3.2.1新增)
该插件有性能损耗,不建议生产环境使用
添加插件: 该插件不用添加自动集成
代码实战:
@PostMapping("/page")
Map<String,Object> page(Integer currPageNo,Integer pageSize) {
HashMap<String,Object> map = new HashMap<>();
Page<TestDemo> page = new Page<>(currPageNo, pageSize);
Page<TestDemo> page1 = testDemoService.page(page);
Integer total = Math.toIntExact(page1.getTotal());
Integer current = Math.toIntExact(page1.getCurrent());
map.put("total",total); // 总条数
map.put("current",current); // 当前页码
map.put("content",page1.getRecords()); // 查询出来的数据
map.put("code",200);
map.put("message","查看分页数据成功");
return map;
}
结果:
控制台打印
Consume Time:1 ms 2023-05-31 17:23:37
Execute SQL:SELECT COUNT(*) AS total FROM test_demo
Consume Time:0 ms 2023-05-31 17:23:37
Execute SQL:SELECT id,username,password,email,update_time,create_time,mobile FROM test_demo LIMIT 3,3
乐观锁: 顾名思义十分乐观,它总是认为不会出现问题,无论干什么都不去上锁!如果出现了问题,再更新值测试。
悲观锁: 顾名思义十分悲观,它总是认为会出现问题,无论干什么都会上锁,然后再去操作!
插件功能: MyBatis-Plus 乐观锁插件是 MyBatis-Plus 提供的一种并发控制手段,用于解决在多线程或分布式场景下数据库并发更新所带来的数据不一致性问题。乐观锁的原理是在数据版本号的基础上进行并发控制,每次更新时判断数据版本号是否匹配,如果不匹配则认为数据已经被其他线程更新过,这时更新操作就会失败,返回更新失败的结果。
实现方式: 乐观锁实现方式如下
取出记录时,获取当前version
更新时,带上这个version
执行更新时, set version = newVersion where version = oldVersion
如果version不对,就更新失败
乐观锁: 1、先查询,获得版本号 version=1
--A
update test_demo set username ="user1" ,version =version+1
where id =1 and version=1
--B 如果线程抢先完成,这个时候version=2,会导致A修改失败
update test_demo set username ="user1" ,version =version+1
where id =1 and version=1
1、数据库添加字段:
ALTER TABLE `test_demo` ADD COLUMN `version` INT(11) DEFAULT '1' COMMENT '版本号';
2、实体类添加字段:
在TestDemo类下面添加如下字段:
@Version
private Integer version;
注意事项:
支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
整数类型下 newVersion = oldVersion + 1
newVersion 会回写到 entity 中
仅支持 updateById(id) 与 update(entity, wrapper) 方法
在 update(entity, wrapper) 方法下, wrapper 不能复用
3、添加插件:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //插件分页拦截器,我的是mysql
mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 添加执行分析插件
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 添加乐观锁插件
return mybatisPlusInterceptor;
}
}
4、代码实战:
单线程执行:
@SpringBootTest(classes = TestDemoApplication.class)
class TestDemoApplicationTests {
@Resource
private TestDemoService testDemoService;
@Test
void update() {
TestDemo testDemo = testDemoService.getById(1);
testDemo.setUsername("testDemo1");
testDemo.setPassword("test1");
testDemoService.updateById(testDemo);
}
}
控制台分析:
可以看到这里的修改条件变成了id加version了,而version的值也变成2了 WHERE id=? AND version=?
结果:
id | username | password | mobile | create_time | update_time | version | |
---|---|---|---|---|---|---|---|
1 | testDemo1 | test1 | [email protected] | 13888888888 | 2023-05-31 16:58:11 | 2023-06-01 11:07:41 | 2 |
可以看到版本号变成2了,说明单线程进行更新操作没有任何问题的,都会执行更新操作,那我们再来试试多线程操作,这里我们简单模拟多线程操作
多线程执行:
@Test
void update() {
// 模拟多线程操作
// 模拟线程1
TestDemo testDemo = testDemoService.getById(1);
testDemo.setUsername("testDemo2");
testDemo.setPassword("test2");
// 模拟线程2插队
TestDemo testDemo1 = testDemoService.getById(1);
testDemo1.setUsername("testDemo3");
testDemo1.setPassword("test3");
testDemoService.updateById(testDemo1); // 线程2抢先提交,这时他的版本发生了变化
testDemoService.updateById(testDemo); // 线程1失败,乐观锁在这种情况下防止了脏数据存在,没有乐观锁就会有覆盖掉线程2的操作
}
控制台分析:
可以看到这里的修改条件变成了id加version了,而version的值也变成3了 WHERE id=? AND version=?
线程2执行结果:
线程1执行结果:
线程1查询的版本号还是2,但其实版本号为2在线程2执行时就更新了变成了 version=3,所以这里查询不到,更新失败
结果:
id | username | password | mobile | create_time | update_time | version | |
---|---|---|---|---|---|---|---|
1 | testDemo3 | test3 | [email protected] | 13888888888 | 2023-05-31 16:58:11 | 2023-06-01 11:52:35 | 3 |
插件功能: 可以通过接口方法名直接创建对应mapper中的sql标签,还可以Mapper和xml可以来回跳,更多功能自行探索!
计划支持:
连接数据源之后 xml 里自动提示字段
sql 增删改查
集成 MP 代码生成
其它
安装方法:
需要MybatisX插件支持,只需要安装一下就可以了,打开 IDEA,进入 File -> Settings -> Plugins,安装完成后重启
MyBatis-Plus 提供了 SQL 注入器来能够自定义 MySQL 的 SQL,可以灵活的进行条件组装,从而实现动态SQL查询的目的。
1、定义接口方法,在mapper接口添加接口方法
在 package com.example.demo.mapper; 包下的 TestDemoMapper 接口添加 deleteAll() 方法
public interface TestDemoMapper extends BaseMapper<TestDemo> {
public void deleteAll();
}
注意:我们不需要编写xml映射,因为我们会采用sql注入的形式,在 MybatisPlus 启动的时候就注入。
注:添加自定义SQL配置类和 注册自定义方法都在这个包下面 package com.example.demo.util.mp;
2、添加自定义SQL配置类
在 package com.example.demo.util.mp; 包下创建 MyMappedStatement 类,并继承 AbstractMethod,实现自定义SQL注入代码
public class MyMappedStatement extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// 接口中的方法名
String method = "deleteAll";
// 该方法执行语句
String sql = "delete from " + tableInfo.getTableName();
// 创建SqlSource
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
// 构造一个删除的MappedStatement并返回
return this.addDeleteMappedStatement(mapperClass, method, sqlSource);
}
}
3、注册自定义方法
在 package com.example.demo.util.mp; 包下创建 MySqlInjector类,并继承 DefaultSqlInjector,实现注册SQL自定义方法
public class MySqlInjector extends DefaultSqlInjector {
/**
* 如果只需增加方法,保留MP自带方法
* 可以super.getMethodList() 再add
* @return
*/
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass,tableInfo);
methodList.add(new MyMappedStatement());
return methodList;
}
}
4、添加自定义SQL注入对象
注释“执行分析插件”:会影响代码执行,它会阻止全表删除操作,所以先注释掉,反正我们已经学会了
在 package com.example.demo.config; 包下的 MybatisPlusConfig类,并注册相关 Bean,实现SQL自动注入功能
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //插件分页拦截器,我的是mysql
// mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 添加执行分析插件
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 添加乐观锁插件
return mybatisPlusInterceptor;
}
@Bean
public MySqlInjector mySqlInjector() {
return new MySqlInjector();
}
}
5、测试
在 package com.example.testdemo; 包下的 TestDemoApplicationTests 类编写测试代码,并进行测试
@SpringBootTest(classes = TestDemoApplication.class)
class TestDemoApplicationTests {
@Autowired
private TestDemoMapper testDemoMapper;
@Test
void testDeleteAll() {
testDemoMapper.deleteAll();
}
}
测试前数据:
控制台输出:
==> Preparing: delete from test_demo
==> Parameters:
<== Updates: 5
测试后数据:
逻辑删除: 逻辑删除指将数据标记为“已删除”,而非真正删除。可以在表中增加一个 deleted 标志位字段,并在查询时过滤掉已被标记删除的数据。可以使用 ORM 框架的软删除插件来简化实现。
注意:在操作之前进行记得先添加数据,已进行逻辑删除的数据,使用id查询的时候默认加上了 deleted=0 的条件
1、添加application.yml配置
如果有以下配置,请忽略此步骤
com.baomidou.mybatisplus.core.config.GlobalConfig$DbConfig
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
2、实体类字段上加上@TableLogic
注解
在TestDemo类下面添加如下字段:
@TableLogic
private Integer deleted;
3、数据库添加字段
ALTER TABLE `test_demo` ADD COLUMN `deleted` INT(11) DEFAULT '0' COMMENT '是否删除(0:未删除、1:已删除)';
4、测试
@SpringBootTest(classes = TestDemoApplication.class)
class TestDemoApplicationTests {
@Resource
private TestDemoMapper testDemoMapper;
@Test
void testDeleteAll() {
testDemoMapper.deleteById(14);
}
}
控制台输出: 可以看到我们的deleteById 的SQL变成了更新语句,并且将 deleted 字段修改成逻辑已删除值
==> Preparing: UPDATE test_demo SET deleted=1 WHERE id=? AND deleted=0
==> Parameters: 14(Integer)
<== Updates: 1
最终结果:
实现自定义枚举有两种方式,一种是使用注解而另一种是使用实现接口IEnum的方式,在接下来的案例中,我们会分别使用这两种进行讲解,这样可以让大家学的更全面一些。
注意:3.3.0之前的版本可能还需要额外的配置
1、数据库添加字段
ALTER TABLE `test_demo` ADD COLUMN (`age` INT(11),`grade` INT(11));
2、声明枚举
在这个目录下面 添加枚举:package com.example.demo.enums;
方式一: 使用 @EnumValue 注解枚举属性 完整示例
public enum GradeEnum {
PRIMARY(1, "小学"), SECONDORY(2, "中学"), HIGH(3, "高中");
private final String descp;
GradeEnum(int code, String descp) {
this.code = code;
this.descp = descp;
}
@EnumValue // 标记数据库存的值是code
private final int code;
}
方式二: 枚举属性,实现 IEnum 接口如下
public enum AgeEnum implements IEnum<Integer> {
ONE(1, "一岁"),
TWO(2, "二岁"),
THREE(3, "三岁");
private int value;
private String desc;
AgeEnum(int value, String desc) {
this.value = value;
this.desc = desc;
}
@Override
public Integer getValue() {
return this.value;
}
}
3、在实体类下面添加字段
在TestDemo类下面添加如下字段:
/**
* 年龄,IEnum接口的枚举处理
* 数据库字段:age INT(3)
*/
private AgeEnum age;
/**
* 年级,原生枚举(带{@link com.baomidou.mybatisplus.annotation.EnumValue}):
* 数据库字段:grade INT(2)
*/
private GradeEnum grade;
4、测试
@SpringBootTest(classes = TestDemoApplication.class)
class TestDemoApplicationTests {
@Resource
private TestDemoService testDemoService;
@Test
void insert() {
TestDemo testDemo = new TestDemo();
testDemo.setUsername("testDemo1");
testDemo.setPassword("test1");
testDemo.setEmail("[email protected]");
testDemo.setMobile("12345698756");
testDemo.setCreateTime(LocalDateTime.now());
testDemo.setAge(AgeEnum.ONE);
testDemo.setGrade(GradeEnum.HIGH);
testDemoService.save(testDemo);
}
}
控制台输出:
==> Preparing: INSERT INTO test_demo ( username, password, email, create_time, mobile, age, grade ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
==> Parameters: testDemo1(String), test1(String), test1@qq.com(String), 2023-06-02T10:30:26.042(LocalDateTime), 12345698756(String), 1(Integer), 3(Integer)
<== Updates: 1
结果:
MyBatis-Plus提供了一种自动填充(AutoFill)功能,可以在执行插入或者更新操作时,自动填充指定的字段。这个功能通常用来填充一些公共字段,例如创建时间、更新时间、创建人、更新人等等,避免重复代码的编写,提高代码的可维护性。
1、添加注解
package com.example.demo.model;
将TestDemo实体类上面的更新时间和创建时间两个字段的注解修改成如下所示
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime createTime;
2、创建自动填充功能处理器
在如下包中创建 MyMetaObjectHandler 类 package com.example.demo.util.mp;
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
setFieldValByName("createTime", LocalDateTime.now(), metaObject);
setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
}
3、注册自动填充功能处理器:
在 package com.example.demo.config; 包下面的 MybatisPlusConfig 配置类中注册自动填充功能处理器
@Bean
public MyMetaObjectHandler myMetaObjectHandler() {
return new MyMetaObjectHandler();
}
4、测试
package com.example.testdemo;
@SpringBootTest(classes = TestDemoApplication.class)
class TestDemoApplicationTests {
@Resource
private TestDemoService testDemoService;
@Test
void insert() { // 新增
TestDemo testDemo = new TestDemo();
testDemo.setUsername("testDemo1");
testDemo.setPassword("test1");
testDemo.setEmail("[email protected]");
testDemo.setMobile("12345698756");
testDemo.setAge(AgeEnum.ONE);
testDemo.setGrade(GradeEnum.HIGH);
testDemoService.save(testDemo);
}
@Test
void update() { // 更新
TestDemo testDemo = testDemoService.getById(18);
testDemo.setUsername("testDemo2");
testDemo.setPassword("test2");
testDemo.setEmail("[email protected]");
testDemo.setMobile("98745632102");
testDemo.setAge(AgeEnum.TWO);
testDemo.setGrade(GradeEnum.PRIMARY);
testDemoService.updateById(testDemo);
}
}
新增结果:
更新结果:
此版本至少在 3.2.0 开始才生效噢~
字段类型处理器,用于 JavaType 与 JdbcType 之间的转换,用于 PreparedStatement 设置参数值和从 ResultSet 或 CallableStatement 中取出一个值,本文讲解 mybaits-plus 内置常用类型处理器如何通过TableField注解快速注入到 mybatis 容器中。
@Data
@Accessors(chain = true)
@TableName(autoResultMap = true)
public class User {
private Long id;
...
/**
* 注意!! 必须开启映射注解
*
* @TableName(autoResultMap = true)
*
* 以下两种类型处理器,二选一 也可以同时存在
*
* 注意!!选择对应的 JSON 处理器也必须存在对应 JSON 解析依赖包
*/
@TableField(typeHandler = JacksonTypeHandler.class)
// @TableField(typeHandler = FastjsonTypeHandler.class)
private OtherInfo otherInfo;
}
该注解对应了 XML 中写法为
<result column="other_info" jdbcType="VARCHAR" property="otherInfo" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler" />
1、ID生成策略对比
策略 | 描述 | 实例 |
---|---|---|
IdType.ASSIGN_ID |
自定义 ID | @TableId(type = IdType.ASSIGN_ID, value = "自定义ID") |
IdType.ASSIGN_UUID |
使用 UUID 作为 ID | @TableId(type = IdType.ASSIGN_UUID) |
IdType.AUTO |
自动增长 | @TableId(type = IdType.AUTO) |
IdType.ID_WORKER |
全局唯一 ID,无前缀 | @TableId(type = IdType.ID_WORKER) |
IdType.ID_WORKER_STR |
全局唯一 ID,带字符串前缀 | @TableId(type = IdType.ID_WORKER_STR) |
IdType.INPUT |
手动输入 | @TableId(type = IdType.INPUT) |
IdType.NONE |
不使用 ID | @TableId(type = IdType.NONE) |
IdType.UUID |
使用 UUIDHexGenerator 生成 UUID | @TableId(type = IdType.UUID) |
2、导入数据
# 这是一个用于删除名为 "goods" 的数据库表的 SQL 命令。它先检查该表是否存在,如果存在就删除它。通常用于重新创建一个表。
DROP TABLE IF EXISTS `goods`;
# 新建表
CREATE TABLE `goods` (
`pid` bigint NOT NULL COMMENT '商品主键',
`goods_name` varchar(255) DEFAULT NULL COMMENT '商品名称',
PRIMARY KEY (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品';
3、生成代码 运行NewAutoCodeGenerator
使用代码生成器生成相关的代码:https://blog.csdn.net/ITKidKid/article/details/126185295
请输入表名,用逗号隔开:
goods
温馨提示:自3.3.0开始,默认使用雪花算法+UUID(不含中划线)
4、重写方法
方法 | 主键生成策略 | 主键类型 | 说明 |
---|---|---|---|
nextId | ASSIGN_ID | Long,Integer,String | 支持自动转换为String类型,但数值类型不支持自动转换,需精准匹配,例如返回Long,实体主键就不支持定义为Integer |
nextUUID | ASSIGN_UUID,UUID | String | 默认不含中划线的UUID生成 |
5、添加雪花算法工具类
在 package com.example.demo.util.mp; 包下添加 SnowflakeUtils 雪花算法工具类
/**
* 雪花算法生成唯一的有序的序列号
*/
public class SnowFlakeUtil {
private static SnowFlakeUtil snowFlakeUtil;
static {
snowFlakeUtil = new SnowFlakeUtil();
}
// 初始时间戳(纪年),可用雪花算法服务上线时间戳的值
// 1650789964886:2022-04-24 16:45:59
private static final long INIT_EPOCH = 1650789964886L;
// 时间位取&
private static final long TIME_BIT = 0b1111111111111111111111111111111111111111110000000000000000000000L;
// 记录最后使用的毫秒时间戳,主要用于判断是否同一毫秒,以及用于服务器时钟回拨判断
private long lastTimeMillis = -1L;
// dataCenterId占用的位数
private static final long DATA_CENTER_ID_BITS = 5L;
// dataCenterId占用5个比特位,最大值31
// 0000000000000000000000000000000000000000000000000000000000011111
private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
// dataCenterId
private long dataCenterId;
// workId占用的位数
private static final long WORKER_ID_BITS = 5L;
// workId占用5个比特位,最大值31
// 0000000000000000000000000000000000000000000000000000000000011111
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
// workId
private long workerId;
// 最后12位,代表每毫秒内可产生最大序列号,即 2^12 - 1 = 4095
private static final long SEQUENCE_BITS = 12L;
// 掩码(最低12位为1,高位都为0),主要用于与自增后的序列号进行位与,如果值为0,则代表自增后的序列号超过了4095
// 0000000000000000000000000000000000000000000000000000111111111111
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
// 同一毫秒内的最新序号,最大值可为 2^12 - 1 = 4095
private long sequence;
// workId位需要左移的位数 12
private static final long WORK_ID_SHIFT = SEQUENCE_BITS;
// dataCenterId位需要左移的位数 12+5
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
// 时间戳需要左移的位数 12+5+5
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
/**
* 无参构造
*/
public SnowFlakeUtil() {
this(1, 1);
}
/**
* 有参构造
* @param dataCenterId
* @param workerId
*/
public SnowFlakeUtil(long dataCenterId, long workerId) {
// 检查dataCenterId的合法值
if (dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER_ID) {
throw new IllegalArgumentException(
String.format("dataCenterId 值必须大于 0 并且小于 %d", MAX_DATA_CENTER_ID));
}
// 检查workId的合法值
if (workerId < 0 || workerId > MAX_WORKER_ID) {
throw new IllegalArgumentException(String.format("workId 值必须大于 0 并且小于 %d", MAX_WORKER_ID));
}
this.workerId = workerId;
this.dataCenterId = dataCenterId;
}
/**
* 获取唯一ID
* @return
*/
public static Long getSnowFlakeId() {
return snowFlakeUtil.nextId();
}
/**
* 通过雪花算法生成下一个id,注意这里使用synchronized同步
* @return 唯一id
*/
public synchronized long nextId() {
long currentTimeMillis = System.currentTimeMillis();
System.out.println(currentTimeMillis);
// 当前时间小于上一次生成id使用的时间,可能出现服务器时钟回拨问题
if (currentTimeMillis < lastTimeMillis) {
throw new RuntimeException(
String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", currentTimeMillis,
lastTimeMillis));
}
if (currentTimeMillis == lastTimeMillis) {
// 还是在同一毫秒内,则将序列号递增1,序列号最大值为4095
// 序列号的最大值是4095,使用掩码(最低12位为1,高位都为0)进行位与运行后如果值为0,则自增后的序列号超过了4095
// 那么就使用新的时间戳
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
currentTimeMillis = getNextMillis(lastTimeMillis);
}
} else { // 不在同一毫秒内,则序列号重新从0开始,序列号最大值为4095
sequence = 0;
}
// 记录最后一次使用的毫秒时间戳
lastTimeMillis = currentTimeMillis;
// 核心算法,将不同部分的数值移动到指定的位置,然后进行或运行
// <<:左移运算符, 1 << 2 即将二进制的 1 扩大 2^2 倍
// |:位或运算符, 是把某两个数中, 只要其中一个的某一位为1, 则结果的该位就为1
// 优先级:<< > |
return
// 时间戳部分
((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT)
// 数据中心部分
| (dataCenterId << DATA_CENTER_ID_SHIFT)
// 机器表示部分
| (workerId << WORK_ID_SHIFT)
// 序列号部分
| sequence;
}
/**
* 获取指定时间戳的接下来的时间戳,也可以说是下一毫秒
* @param lastTimeMillis 指定毫秒时间戳
* @return 时间戳
*/
private long getNextMillis(long lastTimeMillis) {
long currentTimeMillis = System.currentTimeMillis();
while (currentTimeMillis <= lastTimeMillis) {
currentTimeMillis = System.currentTimeMillis();
}
return currentTimeMillis;
}
/**
* 获取随机字符串,length=13
* @return
*/
public static String getRandomStr() {
return Long.toString(getSnowFlakeId(), Character.MAX_RADIX);
}
/**
* 从ID中获取时间
* @param id 由此类生成的ID
* @return
*/
public static Date getTimeBySnowFlakeId(long id) {
return new Date(((TIME_BIT & id) >> 22) + INIT_EPOCH);
}
// public static void main(String[] args) {
// SnowFlakeUtil snowFlakeUtil = new SnowFlakeUtil();
// long id = snowFlakeUtil.nextId();
// System.out.println(id);
// Date date = SnowFlakeUtil.getTimeBySnowFlakeId(id);
// System.out.println(date);
// long time = date.getTime();
// System.out.println(time);
// System.out.println(getRandomStr());
//
// }
}
6、创建自定义ID生成器实体类
在 package com.example.demo.util.mp; 包下面创建 CustomIdGenerator ID生成器实体类
public class CustomIdGenerator implements IdentifierGenerator {
@Value("${server.worker-id:1}")
private Integer workerId;
@Value("${server.data-center-id:1}")
private Integer dataCenterId;
@Override
public Long nextId(Object entity) {
//可以将当前传入的class全类名来作为bizKey,或者提取参数来生成bizKey进行分布式Id调用生成.
String bizKey = entity.getClass().getName();
//根据bizKey调用分布式ID生成
System.out.println("bizKey:" + bizKey);
// 获取元对象
MetaObject metaObject = SystemMetaObject.forObject(entity);
// 获取商品名称
String name = (String) metaObject.getValue("goodsName");
//上面的雪花算法
long id = new SnowFlakeUtil(workerId, dataCenterId).nextId();
//返回生成的id值即可.
System.out.println("为" + name + "生成主键值->:" + id);
return id;
}
}
7、注册自定义ID生成器
在 package com.example.demo.config; 包下面的 MybatisPlusConfig 注册 Bean
@Bean
public IdentifierGenerator customIdGenerator(){
return new CustomIdGenerator();
}
8、修改主键策略
修改 package com.example.demo.model; 包下面的 Goods 类中的主键策略
/**
* 商品主键
*/
@TableId(value = "pid", type = IdType.ASSIGN_ID)
private Long pid;
9、测试
@SpringBootTest(classes = TestDemoApplication.class)
class TestDemoApplicationTests {
@Resource
private GoodsService goodsService;
@Test
void testCustomIdGenerator() {
Goods goods = new Goods();
goods.setGoodsName("电脑");
boolean save = goodsService.save(goods);
System.out.println(save);
}
}
结果:
在实际开发中,我们经常会使用到MySQL和Oracle数据库,但是这两种数据库对于主键有不同的策略,如下:
那我们Oracle又要如何使用主键策略,在这里,我们就不进行一步一步介绍了,我们只提出解决方法
在实体类对象上添加注解@KeySequence(value=”序列名”, clazz=主键属性类型.class)
在实体类对象的主键字段上添加注解@TableId(value = “主键名称”, type = IdType.INPUT)
@KeySequence(value = "SEQ_ORACLE_INTEGER_KEY", clazz = Integer.class)
public class YourEntity {
@TableId(value = "ID", type = IdType.INPUT)
private Integer id;
}
在全局配置中注册com.baomidou.mybatisplus.incrementer.OracleKeyGenerator
@Bean
public IKeyGenerator keyGenerator() {
return new oracleKeyGenerator();
}
除了Oracle,MyBatis-Plus还内置支持以下数据库序列:
如果内置支持不满足你的需求,可实现IKeyGenerator接口来进行扩展。
OVER
参考文章:https://baomidou.com/
https://blog.csdn.net/qq_38490457/article/details/108809902