封面画师:T5-茨舞(微博) 封面ID:69983911
1. MyBatis-Plus概述
1.1 为什么要学习?
Hibernate是一种全自动化的ORM框架,但是MyBatis不是,人们认为MyBatis属于半自动化的ORM框架,因为需要手写SQL语句。针对复杂的SQL进行手写倒也还好,但是一些简单的SQL还要手写就变得十分麻烦(我知道有注解,闭嘴!),同时还会让XML映射文件变得十分臃肿,那么有没有方法可以省略简单SQL的书写呢?
这时候,一个“天降猛男”登场了——MyBatis-Plus。💪
MyBatis-Plus可以很好地帮我们简化操作,对于一些简单的CRUD操作,不再需要我们编写。当然,除了这个最“肤浅”的作用外,MyBatis-Plus还有很多其他的特性,总之就是一句话:为了简化操作,为了偷懒。😂
MyBatis-Plus由国人开发,文档很详细,编码符合国人习惯,荣获【2019年度开源中国最受欢迎软件,开发工具类 TOP1】,还能简化开发,所以…不学一个?
官方文档:官网链接1 或者 官网链接2
PS:本文中,MP是MyBatis-Plus的简写。
2.2 MyBatis-Plus概述
官网:MyBatis-Plus
MyBatis-Plus (简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
基友搭配,效率翻倍! 😆
1.3 特性
无侵入 :只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
损耗小 :启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
强大的 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 操作智能分析阻断,也可自定义拦截规则,预防误操作
2. 快速开始
官方快速开始地址:MyBatis-Plus-QuickStart
拓展使用第三方组件的步骤:
导入对应的依赖
编写对应的配置
研究代码的编写
提高技术的拓展
步骤
1、创建一个数据库mybatis_plus
2、创建user表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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)
);
-- 在正式的开发中,version(乐观锁)、deleted(逻辑删除)、gmt_created、gmt_modified
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' );
3、使用IDEA创建一个SpringBoot项目mybatis_plus,选择web和lombok依赖进行导入。
4、导入其他依赖
1 2 3 4 5 6 7 8 9 10 11 12 <!--数据库驱动-->
< dependency >
< groupId >mysql</ groupId >
< artifactId >mysql-connector-java</ artifactId >
</ dependency >
<!-- mybatis-plus -->
<!-- 3.0.3版本移除 对 mybatis-plus-generator 包(代码生成器使用)的依赖,自己按需引入 -->
< dependency >
< groupId >com.baomidou</ groupId >
< artifactId >mybatis-plus-boot-starter</ artifactId >
< version >Latest Version</ version >
</ dependency >
说明:使用MyBatis-Plus可以节省代码的编写,尽量 不要同时 导入MyBatis-Plus和MyBatis,避免存在依赖错误。
5、连接数据库
1 2 3 4 spring.datasource.username = root
spring.datasource.password = 123456
spring.datasource.url = jdbc:mysql://localhost:3306/mybatis_plus? useSSL = false& useUnicode = true& characterEncoding = utf-8& serverTimezone = GMT%2B8
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
没使用MyBatis-Plus时,我们需要编写pojo、dao、service、controller,但是使用了Mybatis-Plus后,就不需要这么麻烦了。
6、使用Mybatis-Plus后:
1 2 3 4 5 6 7 8 9 10 @ Data
@ AllArgsConstructor
@ NoArgsConstructor
public class User {
private Long id ; // 对应数据库的主键
private String name ;
private Integer age ;
private String email ;
}
1 2 3 4 5 // 使用Mybatis-Plus后,只需要在对应的Mapper上继承BaseMapper即可
@ Repository // 表示持久层
public interface UserMapper extends BaseMapper < User > {
// 这个时候,简单的CURD已经编写完成了
}
注意:编写完mapper接口后,还需要在主启动类上扫描mapper文件夹: @MapperScan("com.yang.mapper")
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @ SpringBootTest
class MybatisPlusApplicationTests {
@ Autowired
private UserMapper userMapper ;
@ Test
void contextLoads () {
// 查询全部用户
// selectList的参数是Wrapper,这是一个条件构造器
List < User > users = userMapper . selectList ( null );
users . forEach ( System . out :: println);
}
}
查询结果:
思考
我们没有编写SQL,是怎么查询出来的? MyBatis-Plus已经帮我们写好了
SQL操作的方法又是拿来的? MyBatis-Plus已经帮我们写好了
3. 配置日志
使用MyBatis-Plus后,部分SQL是不可见的,我们希望知道它是如何执行的,这个时候就需要查看日志!
在配置文件中进行配置:
1 2 3 # MyBatis-Plus配置日志
# 使用默认控制台输出,不需要导入依赖
mybatis-plus.configuration.log-impl = org.apache.ibatis.logging.stdout.StdOutImpl
配置日志后,在以后的学习中,我们可以查看日志,观察MyBatis-Plus的SQL执行。
4. 数据基本操作
所有操作,均在测试类中进行。
4.1 数据插入
1 2 3 4 5 6 7 8 9 10 11 12 13 @ Test
public void testInsert (){
User user = new User () ;
// 注意:这里没有插入id
user . setName ( "默烦" );
user . setAge ( 18 );
user . setEmail ( "cy.mofan@qq.com" );
int result = userMapper . insert (user);
// 受影响的行数
System . out . println (result);
System . out . println (user);
}
然后运行测试:
我们发现:我们明明没有设置ID,插入时居然自动生成了ID,而且生成的ID似乎有点“ 奇怪 ”。
数据库插入的ID应该是全局唯一的,这个时候我们需要了解一些主键生成策略。👇
4.2 主键生成策略
可以使用UUID、自增ID、雪花算法、Redis、ZK等方式来生成主键。
默认 ID_WORKER 全局唯一ID
参考链接:分布式系统唯一ID生成方案汇总
MyBatis-Plus默认采用的使用推特的雪花算法。
雪花算法 :
snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。
我们可以前往我们的User实体类,然后在id属性上加一个注解:@TableId,点击进入这个注解:
1 2 3 4 5 6 7 8 @ Documented
@ Retention ( RetentionPolicy . RUNTIME )
@ Target ({ ElementType . FIELD })
public @ interface TableId {
String value () default "" ;
IdType type () default IdType . NONE ;
}
点击并进入Idtype:
1 2 3 4 5 6 7 8 9 10 11 12 public enum IdType {
AUTO ( 0 ) , // 主键自增
NONE ( 1 ) , // 未设置主键
INPUT ( 2 ) , // 手动输入
ID_WORKER ( 3 ) , // 默认,雪花算法
UUID ( 4 ) , // UUID
ID_WORKER_STR ( 5 ) ; // ID_WORKER的字符串表示法
private int key ;
private IdType ( int key ) { this . key = key;}
public int getKey () { return this . key ;}
}
我们发现这是一个枚举类,其中的ID_WORKER就是默认生成策略。
主键自增
使用MyBatis-Plus,设置组件生成策略为 自增 的方法:
1、实体类中表示组件的属性上添加注解:@TableId(type = IdType.AUTO)
2、 数据库字段一定要是自增的
3、注解添加完成、数据库字段修改(记得保存)都完成后,我们在此运行插入测试:
手动输入
使用MyBatis-Plus,设置组件生成策略为 手动输入 的方法:
只需要在实体类中表示组件的属性上添加注解:@TableId(type = IdType.AUTO)即可。
如果我们设置组件生成策略为 手动输入 ,但是没有输入,这个时候日志就会显示插入的是null。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @ Test
public void testInsert (){
User user = new User () ;
// user.setName("默烦");
user . setId ( 6L );
user . setName ( "mofan" );
user . setAge ( 18 );
user . setEmail ( "cy.mofan@qq.com" );
int result = userMapper . insert (user);
// 受影响的行数
System . out . println (result);
System . out . println (user);
}
运行结果:
4.3 数据更新
更新单个数据
根据ID更新单个数据:
1 2 3 4 5 6 7 8 9 10 11 @ Test
public void testUpdate (){
User user = new User () ;
user . setId ( 6L );
user . setName ( "Yang" );
// updateById 的参数类型是对象!
int result = userMapper . updateById (user);
System . out . println (result);
System . out . println (user);
}
控制台日志打印:
数据库显示:
更新多个数据
根据ID更新多个数据:
1 2 3 4 5 6 7 8 9 10 11 @ Test
public void testUpdate (){
User user = new User () ;
user . setId ( 6L );
user . setName ( "mofan" );
user . setAge ( 20 );
int result = userMapper . updateById (user);
System . out . println (result);
System . out . println (user);
}
控制台日志打印:
数据库显示:
MP(以下 MyBatis-Plus 简写为 MP)会自动根据给对象设置的属性值来更新数据。
更新字段值为 null
有两种可供方式可供参考:
使用 LambdaUpdateWrapper
使用 UpdateWrapper
现有如下的实体类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @ Getter
@ Setter
@ TableName ( "tb_test_null" )
public class TestNull extends BaseEntity {
@ Serial
private static final long serialVersionUID = - 1332169884425770306L ;
@ TableField ( "name" )
private String name ;
@ TableField ( "age" )
private Integer age ;
@ TableField ( "empty_field" )
private String emptyField ;
}
先插入一条数据:
1 2 3 4 5 6 7 8 9 10 private static final String UNIQUE_NAME = "mofan" ;
@ BeforeEach
public void init () {
TestNull testNull = new TestNull () ;
testNull . setName (UNIQUE_NAME);
testNull . setAge ( 21 );
testNull . setEmptyField ( "" );
testNullDao . insert (testNull);
}
然后尝试将 age 和 emptyField 值更新为 null:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 private List < TestNull > getMofanTestNull () {
LambdaQueryWrapper < TestNull > wrapper = Wrappers . lambdaQuery ( TestNull . class )
. eq (TestNull :: getName, UNIQUE_NAME);
return testNullDao . selectList (wrapper);
}
@ Test
public void testLambdaWrapperUpdateNull () {
Optional < TestNull > optional = getMofanTestNull () . stream ()
. filter (i -> UNIQUE_NAME . equals ( i . getName ()))
. findFirst ();
var wrapper = Wrappers . lambdaUpdate ( TestNull . class )
. set (TestNull :: getEmptyField, null )
. set (TestNull :: getAge, null );
optional . ifPresent (i -> testNullDao . update (i, wrapper));
optional = getMofanTestNull () . stream ()
. filter (i -> UNIQUE_NAME . equals ( i . getName ()))
. findFirst ();
optional . ifPresent (
entity -> assertThat (entity)
. extracting (TestNull :: getEmptyField, TestNull :: getAge)
. containsOnlyNulls ()
);
}
@ Test
public void testWrapperUpdateNull () {
Optional < TestNull > optional = getMofanTestNull () . stream ()
. filter (i -> UNIQUE_NAME . equals ( i . getName ()))
. findFirst ();
if ( optional . isEmpty () ) {
Assertions . fail ();
}
TestNull entity = optional . get ();
var wrapper = new UpdateWrapper <> (entity) ;
wrapper . set ( "empty_field" , null ). set ( "age" , null );
testNullDao . update (entity, wrapper);
optional = getMofanTestNull () . stream ()
. filter (i -> UNIQUE_NAME . equals ( i . getName ()))
. findFirst ();
optional . ifPresent (
i -> assertThat (i)
. extracting (TestNull :: getEmptyField, TestNull :: getAge)
. containsOnlyNulls ()
);
}
4.4 自动填充
创建时间、更新时间,对于这两个字段的操作我们希望是自动完成而不是需要手动编写。
阿里巴巴开发手册说过:任何数据库表都必须有两个字段:gmt_created(创建时间)、gmt_modified(修改时间),而且需要这两个字段的更新自动化。
自动填充有两种方式:数据库级别、代码级别
数据库级别(实际开发不允许)
1、在表中新增两个字段create_time、update_time,并将这两个字段类型设置为timestamp,开启根据当前时间戳更新:
2、同步实体类:
1 2 3 // 数据库使用下划线,实体类使用驼峰命名,依旧可以进行匹配
private Date createTime ;
private Date updateTime ;
3、执行更新测试操作:
1 2 3 4 5 6 7 8 9 10 11 @ Test
public void testUpdate (){
User user = new User () ;
user . setId ( 6L );
user . setName ( "mofan" );
user . setAge ( 19 );
int result = userMapper . updateById (user);
System . out . println (result);
System . out . println (user);
}
4、查看结果:
代码级别
1、由于前面的测试,我们需要移除数据库的默认值和更新操作:
2、实体类的属性上增加注解
1 2 3 4 5 // 字段填充内容
@ TableField ( fill = FieldFill . INSERT )
private Date createTime ;
@ TableField ( fill = FieldFill . INSERT_UPDATE )
private Date updateTime ;
3、编写处理器来处理这些注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @ Slf4j
@ Component // 处理器添加到容器中
public class MyMetaObjectHandler implements MetaObjectHandler {
// 插入的填充策略
@ Override
public void insertFill ( MetaObject metaObject ) {
log . info ( "开始进行插入..." );
// setFieldValByName(String fieldName, Object fieldVal, MetaObject metaObject)
this . setFieldValByName ( "createTime" , new Date (),metaObject);
this . setFieldValByName ( "updateTime" , new Date (),metaObject);
}
// 更新的填充策略
@ Override
public void updateFill ( MetaObject metaObject ) {
log . info ( "开始进行更新..." );
this . setFieldValByName ( "updateTime" , new Date (),metaObject);
}
}
4、测试插入:
1 2 3 4 5 6 7 8 9 10 11 @ Test
public void testInsert (){
User user = new User () ;
user . setName ( "mofan" );
user . setAge ( 17 );
user . setEmail ( "cy.mofan@qq.com" );
int result = userMapper . insert (user); // 自动生成ID
System . out . println (result);
System . out . println (user);
}
5、观察插入数据:
4.5 乐观锁
与乐观锁相对的有:悲观锁
乐观锁:一个“乐观”的锁,它总是认为不会出问题,无论干什么都不会加锁。如果出了问题,再次更新值测试
悲观锁:一个“悲观”的锁,它总是认为会出现问题,无论干什么都会加上锁,然后再去操作!
在这主要讲解 乐观锁 机制。
乐观锁插件的适用场景
官方解释:乐观锁插件
意图:
当要更新一条记录的时候,希望这条记录没有被别人更新
乐观锁实现方式:
取出记录时,获取当前version
更新时,带上这个version
执行更新时, set version = newVersion where version = oldVersion
如果version不对,就更新失败
1 2 3 4 5 -- 假设有两个线程A、B同时修改一条记录
update user set name = "mofan" , version = version + 1
where id = 6 and version = 1
-- 但是B抢先完成了修改,version修改成2
-- 这个时候A就不能修改数据了,实现线程通信的安全
测试MP的乐观锁插件
1、给数据库增加version字段,并设置默认值为1
2、给实体类增加相应的字段,并添加注解@Version
1 2 @ Version
private Integer version ;
3、注册组件
1 2 3 4 5 6 7 8 9 10 11 12 @ EnableTransactionManagement // 默认已添加该注解
@ Configuration
// 移动主配置文件下扫描mapper文件夹的注解(非必须,仅仅为了美观,与乐观锁配置无关)
@ MapperScan ( "com.yang.mapper" )
public class MyBatisPlusConfig {
// 注册乐观锁插件
@ Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor () {
return new OptimisticLockerInterceptor ();
}
}
4、编写测试方法进行测试:
1 2 3 4 5 6 7 8 9 10 11 // 测试乐观锁成功
@ Test
public void testOptimisticLocker (){
// 查询一个用户
User user = userMapper . selectById ( 1l );
// 修改信息
user . setName ( "mofan" );
user . setEmail ( "cy.mofan@qq.com" );
// 更新信息
userMapper . updateById (user);
}
数据库结果:
我们再来测试一下修改失败的情况,主要模拟多线程情况下线程插队的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 测试乐观锁失败 多线程下
@ Test
public void testOptimisticLocker2 (){
// 查询一个用户
User user = userMapper . selectById ( 1l );
// 修改信息
user . setName ( "mofan111" );
user . setEmail ( "cy.mofan@qq.com" );
// 模拟另一个线程插队操作
User user2 = userMapper . selectById ( 1l );
// 修改信息
user2 . setName ( "mofan222" );
user2 . setEmail ( "cy.mofan@qq.com" );
userMapper . updateById (user2);
// 自旋锁来尝试多次提交
userMapper . updateById (user); // 如果没乐观锁,就会覆盖插队线程值
}
如果没有乐观锁,最后一次的修改会覆盖前一次的修改,但是有了乐观锁就不会出现这种情况了。查看数据库修改结果:
查看数据库后,结果与我们预期一样。
4.6 数据查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 根据id查询单个用户
@ Test
public void testSelectById (){
User user = userMapper . selectById ( 1l );
System . out . println (user);
}
// 根据id查询多个用户
@ Test
public void testSelectBatchId (){
List < User > users = userMapper . selectBatchIds ( Arrays . asList ( 1 , 2 , 3 ));
users . forEach ( System . out :: println);
}
// 条件查询之一 使用map进行操作
@ Test
public void testSelectByBatchIds (){
HashMap < String , Object > map = new HashMap <> () ;
// 自定义查询
map . put ( "name" , "默烦" );
map . put ( "age" , "18" );
// 查询name = "默烦",age=18的所有用户
List < User > users = userMapper . selectByMap (map);
users . forEach ( System . out :: println);
}
4.7 数据分页查询
分页查询的方法有很多,比如:
1、原始的 limit 进行分页
2、第三方插件进行分页,比如:PageHelper
3、MP内置的分页插件
使用MP内置的分页插件
1、配置拦截器组件
官方配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //Spring boot方式
@ EnableTransactionManagement
@ Configuration
// 指明需要被扫描的 Mapper 接口文件
@ MapperScan ( "com.baomidou.cloud.service.*.mapper*" )
public class MybatisPlusConfig {
@ 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;
}
}
我测试用的配置:
1 2 3 4 5 6 7 8 9 10 // 分页组件
@ Bean
public PaginationInterceptor paginationInterceptor () {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor () ;
final PaginationInnerInterceptor innerInterceptor = new PaginationInnerInterceptor ( DbType . MYSQL ) ;
// 单页分页条数限制
innerInterceptor . setMaxLimit ( 150L );
interceptor . addInnerInterceptor (innerInterceptor);
return interceptor ;
}
2、直接使用Page对象即可
1 2 3 4 5 6 7 8 9 // 测试分页查询
@ Test
public void testPage (){
// 当前页:2 页面数据量:5条
Page < User > page = new Page <> ( 2 , 5 ) ;
userMapper . selectPage (page, null );
System . out . println ( "总记录数为:" + page . getTotal ());
page . getRecords (). forEach ( System . out :: println);
}
控制台输出:
数据库数据对比:
4.8 数据删除
三种基本的删除:删除单个数据、删除多个数据、根据条件删除。
删除单个数据
1 2 3 4 5 // 删除单个数据
@ Test
public void testDeleteById (){
userMapper . deleteById ( 1272430143247347718L );
}
删除数据前的数据库:参考分页查询最后一张数据库示例图。
删除数据后的数据库:
删除多个数据
1 2 3 4 5 // 删除多个数据
@ Test
public void testDeleteBatchById (){
userMapper . deleteBatchIds ( Arrays . asList ( 1272430143247347715L , 1272430143247347716L ));
}
删除数据后的数据库:
根据条件删除
1 2 3 4 5 6 7 // 根据条件删除
@ Test
public void testDeleteMap (){
HashMap < String , Object > map = new HashMap <> () ;
map . put ( "name" , "默烦" );
userMapper . deleteByMap (map);
}
删除数据后的数据库:
4.9 逻辑删除
物理删除:从数据库中直接移除数据。
逻辑删除:数据没有从数据库中移除,只是打了个标志让用户看不到。(学过HBase可以参考HBase的删除逻辑)
应用场景:管理员可以查看被删除的数据。
测试逻辑删除
1、在数据表中增加逻辑删除字段delete,默认值为0,表示未被删除。
如果在数据库级别没有设置默认值,那么需要添加自动填充,如:
1 2 3 4 5 @ Override
public void insertFill ( MetaObject metaObject) {
...
this . setFieldValByName ( "deleted" , 0 , metaObject);
}
2、实体类中增加属性,并添加注解@TableLogic
1 2 3 4 // 自动填充时使用
// @TableField(fill = FieldFill.INSERT, value = "deleted")
@ TableLogic
private Integer deleted ;
3、编写配置文件
1 2 3 4 5 6 7 mybatis-plus :
global-config :
# 配置逻辑删除
db-config :
logic-delete-field : deleted
logic-delete-value : 1
logic-not-delete-value : 0
4、进行测试
1 2 3 4 @ Test
public void testDeleteById (){
userMapper . deleteById ( 1L );
}
控制台输出信息:
数据表信息:
我们在进行查询,看看能够查询到被逻辑删除的数据:
1 2 3 4 5 @ Test
public void testSelectById (){
User user = userMapper . selectById ( 1l );
System . out . println (user);
}
控制台打印信息:
我们发现,引进逻辑删除后再进行查询会自动过滤被逻辑删除的数据。
5. 条件构造器
官方参考文档:条件构造器
5.1 QueryWrapper
我们写一些复杂的SQL时,就可以用它来替代!
前置:创建一个测试类WrapperTest:
1 2 3 4 5 6 7 @ SpringBootTest
public class WrapperTest {
@ Autowired
private UserMapper userMapper ;
// ...
}
当前数据表数据:
查询name不为空、email不为空、年龄大于等于12的用户
1 2 3 4 5 6 7 8 9 10 @ Test
void contextLoads () {
// 查询name不为空、email不为空、年龄大于等于12的用户
// 与map对比学习
QueryWrapper < User > wrapper = new QueryWrapper <> () ;
wrapper . isNotNull ( "name" )
. isNotNull ( "email" )
. ge ( "age" , 12 );
userMapper . selectList (wrapper). forEach ( System . out :: println);
}
查询name是默烦的一个 用户
1 2 3 4 5 6 7 8 9 @ Test
void test2 (){
// 查询name是默烦的用户
QueryWrapper < User > wrapper = new QueryWrapper <> () ;
wrapper . eq ( "name" , "默烦" );
// 查询一个数据
User user = userMapper . selectOne (wrapper);
System . out . println (user);
}
查询age范围是 20 ~ 30 的用户
1 2 3 4 5 6 7 8 @ Test
void test3 (){
// 查询age范围是 20 ~ 30 的用户
QueryWrapper < User > wrapper = new QueryWrapper <> () ;
wrapper . between ( "age" , 20 , 30 );
Integer count = userMapper . selectCount (wrapper);
System . out . println ( "用户数量为" + count);
}
模糊查询
1 2 3 4 5 6 7 8 9 10 // 模糊查询
@ Test
void test4 (){
QueryWrapper < User > wrapper = new QueryWrapper <> () ;
wrapper . notLike ( "name" , "e" ) // name中不包含e的用户
. likeRight ( "email" , "t" ); // email以t开头的用户
List < Map < String , Object >> maps = userMapper . selectMaps (wrapper);
maps . forEach ( System . out :: println);
}
嵌套查询
1 2 3 4 5 6 7 8 @ Test
void test5 (){
QueryWrapper < User > wrapper = new QueryWrapper <> () ;
// id 在子查询出来
wrapper . inSql ( "id" , "select id from user where id < 3" );
List < Object > objects = userMapper . selectObjs (wrapper);
objects . forEach ( System . out :: println);
}
通过id进行降序排序
1 2 3 4 5 6 7 8 @ Test
void test6 (){
QueryWrapper < User > wrapper = new QueryWrapper <> () ;
// 通过id进行降序排序
wrapper . orderByDesc ( "id" );
List < User > users = userMapper . selectList (wrapper);
users . forEach ( System . out :: println);
}
5.2 LambdaQueryWrapper
在使用 QueryWrapper 时查询字段都是魔法值,都是由开发人员编写的列名。
如果需要重构代码,使用这种方式将十分麻烦,因此在 MP 中有 LambdaQueryWrapper 来解决这个问题。
比如,现将下述代码进行改写:
1 2 3 4 5 6 7 8 9 10 @ Test
void contextLoads () {
// 查询name不为空、email不为空、年龄大于等于12的用户
// 与map对比学习
QueryWrapper < User > wrapper = new QueryWrapper <> () ;
wrapper . isNotNull ( "name" )
. isNotNull ( "email" )
. ge ( "age" , 12 );
userMapper . selectList (wrapper). forEach ( System . out :: println);
}
改写后的代码:
1 2 3 4 5 6 7 @ Test
public void testLambdaQueryWrapper () {
LambdaQueryWrapper < User > wrapper = Wrappers . < User > lambdaQuery () . isNotNull (User :: getName)
. isNotNull (User :: getEmail)
. ge (User :: getAge, 12 );
userMapper . selectList (wrapper). forEach ( System . out :: println);
}
对于 QueryWrapper 对象的获取采用 new 关键字,而 LambdaQueryWrapper 对象的获取则习惯采用 Wrappers 的静态方法获取。
注意: 在使用 MP 的条件构造器时,请注意 and() 方法的使用规则,并区分与 or() 方法的区别。
需要注意的是,在低版本的 MP 中,可能并没有 Wrappers 类(请以参考文档为准)。
6. 代码生成器
官方参考文档:代码生成器
首先导入依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <!-- mybatis-plus -->
<!--3.0.7版本移除 对 mybatis-plus-generator 包的依赖,自己按需引入,尽在还需要导入模板依赖-->
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator -->
< dependency >
< groupId >com.baomidou</ groupId >
< artifactId >mybatis-plus-boot-starter</ artifactId >
< version >Latest Version</ version >
</ dependency >
< dependency >
< groupId >com.baomidou</ groupId >
< artifactId >mybatis-plus-generator</ artifactId >
< version >3.4.1</ version >
</ dependency >
< dependency >
< groupId >org.apache.velocity</ groupId >
< artifactId >velocity-engine-core</ artifactId >
< version >2.0</ version >
</ dependency >
编写生成器类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 package com.yang ;
import com.baomidou.mybatisplus.annotation.DbType ;
import com.baomidou.mybatisplus.annotation.FieldFill ;
import com.baomidou.mybatisplus.annotation.IdType ;
import com.baomidou.mybatisplus.generator.AutoGenerator ;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig ;
import com.baomidou.mybatisplus.generator.config.GlobalConfig ;
import com.baomidou.mybatisplus.generator.config.PackageConfig ;
import com.baomidou.mybatisplus.generator.config.StrategyConfig ;
import com.baomidou.mybatisplus.generator.config.po.TableFill ;
import com.baomidou.mybatisplus.generator.config.rules.DateType ;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy ;
import javax.xml.stream.FactoryConfigurationError ;
import java.util.ArrayList ;
/**
* @author 默烦
* @date 2020/6/16
*/
// 代码自动生成器
public class MofanCode {
public static void main ( String [] args ) {
// 构建一个 代码自动生成器 对象
AutoGenerator mpg = new AutoGenerator ();
/************配置策略***********/
// 1. 全局配置
// 注意:包别导错了
GlobalConfig gc = new GlobalConfig ();
String projectPath = System . getProperty ( "user.dor" ); // 获取当前系统目录
gc . setOutputDir (projectPath + "/src/main/java" ); // 代码输出目录
gc . setAuthor ( "默烦" );
gc . setOpen ( false ); // 是否打开资源文件夹
gc . setFileOverride ( false ); // 是否覆盖原有文件
gc . setServiceName ( "%sService" ); // 去默认Service的I前缀
gc . setIdType ( IdType . ID_WORKER ); // 主键生成策略
gc . setDateType ( DateType . ONLY_DATE ); // 日期类型
gc . setSwagger2 ( true ); // 自动配置Swagger文档
mpg . setGlobalConfig (gc);
// 2. 设置数据源
DataSourceConfig dsc = new DataSourceConfig ();
dsc . setUrl ( "jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8" );
dsc . setDriverName ( "com.mysql.cj.jdbc.Driver" );
dsc . setUsername ( "root" );
dsc . setPassword ( "123456" );
dsc . setDbType ( DbType . MYSQL );
mpg . setDataSource (dsc);
// 3. 包的配置
PackageConfig pc = new PackageConfig ();
pc . setModuleName ( "blog" ); // 模块名
pc . setParent ( "com.yang" );
pc . setEntity ( "pojo" );
pc . setMapper ( "mapper" );
pc . setService ( "service" );
pc . setController ( "controller" );
mpg . setPackageInfo (pc);
// 4. 策略配置
StrategyConfig strategy = new StrategyConfig ();
/*************重点修改************/
strategy . setInclude ( "user" , "tags" ); // 设置映射的表名
/*************重点修改************/
strategy . setNaming ( NamingStrategy . underline_to_camel );
strategy . setColumnNaming ( NamingStrategy . underline_to_camel );
strategy . setEntityLombokModel ( true ); // 自动生成lombok
strategy . setLogicDeleteFieldName ( "deleted" ); // 设置逻辑删除字段
// 自动填充策略
TableFill create_time = new TableFill ( "create_time" , FieldFill . INSERT );
TableFill update_time = new TableFill ( "update_time" , FieldFill . INSERT_UPDATE );
ArrayList < TableFill > tableFills = new ArrayList <>();
tableFills . add (create_time);
tableFills . add (update_time);
strategy . setTableFillList (tableFills);
// 乐观锁设置
strategy . setVersionFieldName ( "version" );
strategy . setRestControllerStyle ( true );
// url 设置 localhost:8080/hello_mofan_1
strategy . setControllerMappingHyphenStyle ( true );
mpg . setStrategy (strategy);
mpg . execute (); // 执行
}
}
以上代码按要求修改即可使用。
7. 拓展批量处理
7.1 批量插入
注意: 拓展的批量插入只适用于 MySQL 数据库。
在 MyBatis-Plus 中存在现成的批量插入方法,我们只需进行简单的拓展即可使用:
1、自定义 Injector 并继承 DefaultSqlInjector 类;
2、重写 getMethodList() 方法,添加现成的批量插入类,并注入 MyBatis-Plus配置类中;
3、最后拓展通用 Mapper 即可。
自定义 Injector 并重写 getMethodList() 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /**
* @author mofan
* @date 2021/3/14 21:54
* 支持自定义数据方法注入
*/
public class EasySqlInjector extends DefaultSqlInjector {
@ Override
public List < AbstractMethod > getMethodList ( Class < ? > mapperClass ) {
// 防止父类的方法不可使用
List < AbstractMethod > methodList = super . getMethodList (mapperClass);
methodList . add ( new InsertBatchSomeColumn ());
return methodList;
}
}
在 MyBatis-Plus 配置类中注入自定义 Injector:
1 2 3 4 @ Bean
public EasySqlInjector easySqlInjector () {
return new EasySqlInjector () ;
}
拓展通用 Mapper,后续的 Mapper 接口不再继承 BaseMapper,而是继承拓展的通用 Mapper。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 /**
* @author mofan
* @date 2021/3/14 21:57
* 拓展通用 Mapper,支持数据批量插入
*/
public interface MybatisBaseMapper < T > extends BaseMapper < T > {
/**
* 批量插入 仅适用于 MySQL
* @param entityList 实体列表
* @return 插入的行数
*/
int insertBatchSomeColumn ( List < T > entityList );
}
使用方法举例:
1 2 3 @ Mapper
@ Repository
public interface UserInfoDao extends MybatisBaseMapper < UserInfo > {}
7.2 批量更新
相较于简单的批量插入拓展,在拓展批量更新时需要我们自己编写批量更新模板方法,然后其他步骤就和批量插入的拓展类似。
首先在数据库连接上添加 allowMultiQueries=true 属性:
1 2 3 4 5 6 datasource :
username : xxx
password : xxx
# 项目中已拓展 Mybatis-plus 的批量更新,需要在连接信息中添加 allowMultiQueries=true 表示开启批量更新
url : jdbc:mysql:///xxx?serverTimezone=GMT%2b8&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true
driver-class-name : com.mysql.cj.jdbc.Driver
批量更新模板方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /**
* @author mofan
* @date 2021/4/14 15:38
* 批量更新方法实现,条件为主键,选择性更新
*/
@ Slf4j
public class UpdateBatchMethod extends AbstractMethod {
private static final long serialVersionUID = 1147163282820238330L ;
@ Override
public MappedStatement injectMappedStatement ( Class < ? > mapperClass , Class < ? > modelClass , TableInfo tableInfo ) {
String sql = "<script> \n <foreach collection= \" list \" item= \" item \" separator= \" ; \" > \n update %s %s where %s=#{%s} %s \n </foreach> \n </script>" ;
String additional = tableInfo . isWithVersion () ? tableInfo . getVersionFieldInfo (). getVersionOli ( "item" , "item." ) : "" + tableInfo . getLogicDeleteSql ( true , true );
String setSql = sqlSet ( tableInfo . isWithLogicDelete (), false , tableInfo, false , "item" , "item." );
String sqlResult = String . format (sql, tableInfo . getTableName (), setSql, tableInfo . getKeyColumn (), "item." + tableInfo . getKeyProperty (), additional);
// log.debug("sqlResult----->{}", sqlResult);
SqlSource sqlSource = languageDriver . createSqlSource (configuration, sqlResult, modelClass);
// 第三个参数必须和MybatisBaseMapper的自定义方法名一致
return this . addUpdateMappedStatement (mapperClass, modelClass, "updateBatch" , sqlSource);
}
}
在自定义的 Injector 添加拓展的批量更新模板类:
1 2 3 4 5 6 7 8 9 10 11 public class EasySqlInjector extends DefaultSqlInjector {
@ Override
public List < AbstractMethod > getMethodList ( Class < ? > mapperClass ) {
// 防止父类的方法不可使用
List < AbstractMethod > methodList = super . getMethodList (mapperClass);
methodList . add ( new InsertBatchSomeColumn ());
methodList . add ( new UpdateBatchMethod ());
return methodList;
}
}
最后拓展通用 Mapper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public interface MybatisBaseMapper < T > extends BaseMapper < T > {
/**
* 批量插入 仅适用于 MySQL
* @param entityList 实体列表
* @return 插入的行数
*/
int insertBatchSomeColumn ( List < T > entityList );
/**
* 自定义批量更新,条件为主键
* @param entityList 实体列表
* @return 是否成功
*/
int updateBatch (@ Param ( "list" ) List < T > entityList );
}
批量更新使用注意事项
1、仅测试过 MySQL 数据库,对于其他数据库的支持尚不明确;
2、无法做到批量更新字段值为 NULL,针对这种情况请自行编写 XML,比如:
1 2 3 4 5 6 7 8 9 10 11 <!-- 重置当年考试科目信息 (将 campus、classroomName 列设置为 null,used 列设置为 false) -->
< update id = "resetExamSubject" >
< foreach collection = "list" item = "item" index = "index" close = "" open = "" separator = ";" >
UPDATE tb_exam_subject
< set >
examination_location = NULL,
used = false
</ set >
WHERE id = ${item.id} AND is_deleted = 0
</ foreach >
</ update >
8. 通用枚举
在 JDK 1.5 之后引入了枚举,如果要在 MP 中实现通用枚举也很简单,只需要配置枚举类的路径,然后使用注解即可。
为了规范枚举的使用,习惯让自定义枚举继承一个接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /**
* @author mofan
* 枚举信息,一般来说,每个枚举类都应该实现这个接口
*/
public interface EnumInfo extends Serializable {
/**
* 获取枚举值对应的代码
* @return 对应的代码
*/
String code ();
/**
* 获取枚举值对应的信息
* @return 对应的信息
*/
String message ();
}
假设有这样一个枚举:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 /**
* @author mofan
* 专业类型
*/
public enum MajorTypeEnum implements EnumInfo {
/**
* 学术型
*/
ACADEMIC ( "Academic" , "学术型" ) ,
/**
* 专业学位
*/
PROFESSIONAL_DEGREE ( "ProfessionalDegree" , "专业学位" ) ;
private String code ;
private String message ;
MajorTypeEnum ( String code , String message ) {
this . code = code;
this . message = message;
}
@ Override
public String code () {
return this . code ;
}
@ Override
public String message () {
return this . message ;
}
}
这个枚举类所在的包路径为:indi.mofan.common.enums。
现在我想让这个枚举保存到数据库中的值是 code,序列化后得到的是 message。那么首先需要在配置文件中指定枚举所在的包路径:
3.5.2 版本开始,无需设置扫描的枚举包!
1 2 3 mybatis-plus :
# 设置枚举包扫描。3.5.2 版本开始,省略此配置。
type-enums-package : indi.mofan.common.enums
然后在 code 属性上使用 @EnumValue 注解:
1 2 @ EnumValue
private String code ;
在 message 上使用 @JsonValue 注解即可:
1 2 @ JsonValue
private String message ;
9. 常用配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 mybatis-plus :
# 映射文件位置
mapper-locations : classpath:indi/mofan/mybatis/*.xml
configuration :
# 映射驼峰(实体属性名)到下划线(列名)
map-underscore-to-camel-case : true
# MyBatis-Plus配置日志
# 使用默认控制台输出,不需要导入依赖
log-impl : org.apache.ibatis.logging.stdout.StdOutImpl
global-config :
# 配置逻辑删除
db-config :
logic-delete-field : deleted
logic-delete-value : 1
logic-not-delete-value : 0
# 设置实体类所对应的表的统一前缀
table-prefix : tb_
# 设置统一的主键生成策略
id-type : assign_id
# 设置枚举包扫描。3.5.2 版本开始,省略此配置。
type-enums-package : indi.mofan.common.enums
注意: 在 SpringBoot 项目中,我们常把 MyBatis 的 SQL 映射文件放在 resources 目录下。