前言
MyBatis-plus进阶内容
1.逻辑删除
2.自动填充
3.乐观锁插件
本篇是本人在mp官网学习过程所产的学习笔记,主要概述了逻辑删除,自动填充,和乐观锁插件的使用,希望对正在学习mp的朋友有所帮助
SQL
#创建用户表
#创建用户表
CREATE TABLE user (
id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主键',
name VARCHAR(30) DEFAULT NULL COMMENT '姓名',
age INT(11) DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
manager_id BIGINT(20) DEFAULT NULL COMMENT '直属上级id',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
update_time DATETIME DEFAULT NULL COMMENT '修改时间',
version INT(11) DEFAULT '1' COMMENT '版本',
deleted INT(1) DEFAULT '0' COMMENT '逻辑删除标识(0.未删除,1.已删除)',
CONSTRAINT manager_fk FOREIGN KEY (manager_id)
REFERENCES user (id)
) ENGINE=INNODB CHARSET=UTF8;
#初始化数据:
INSERT INTO user (id, name, age, email, manager_id
, create_time)
VALUES (1087982257332887553, '大boss', 40, 'boss@baomidou.com', NULL
, '2019-01-11 14:20:20'),
(1088248166370832385, '王天风', 25, 'wtf@baomidou.com', 1087982257332887553
, '2019-02-05 11:12:22'),
(1088250446457389058, '李艺伟', 28, 'lyw@baomidou.com', 1088248166370832385
, '2019-02-14 08:31:16'),
(1094590409767661570, '张雨琪', 31, 'zjq@baomidou.com', 1088248166370832385
, '2019-01-14 09:15:15'),
(1094592041087729666, '刘红雨', 32, 'lhm@baomidou.com', 1088248166370832385
, '2019-01-14 09:48:16');
1.逻辑删除
1.1逻辑删除的简介
逻辑删除其实就是假删除,将数据库中代表是否被删除的的字段状态该为被删除状态,之后你仍然能看到此条记录
1.2在使用逻辑删除时需要注意什么?
配置逻辑删除(application.properties,domain,config)
1.application.properties
如果定义的表中表示逻辑删除字段的字段值与mp的默认逻辑删除的值不相同时,你需要在application.properties中进行配置,相反如果你创建的表中表示逻辑删除字段的字段值与默认逻辑删除的相同就不需要配置
**mp的默认逻辑删除的值:**逻辑未删除 0 ,逻辑已删除1
application.properties中的配置
# 配置逻辑删除 mp的逻辑删除默认是 0 和 1 如果与mybatis-plus的默认不相同 需要在以下配置语句中进行配置相同则不需要进行配置
# 逻辑未被删除是0
mybatis-plus.global-config.db-config.logic-not-delete-value=0
# 逻辑已删除为1
mybatis-plus.global-config.db-config.logic-delete-value=1
2.domain中的表实体对象
我们可以通过@TableLogic注解对表记录逻辑删除的字段值进行局部声明(局部声明:只是单单对于这个实体类而言,并不是全局的)
com.czxy.domain.User
package com.czxy.domain;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.time.LocalDateTime;
/**
* @Author ScholarTang
* @Date 2019/10/31 7:52 PM
* @Desc 用户类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@TableName("user")
public class User {
/*
id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主键',
name VARCHAR(30) DEFAULT NULL COMMENT '姓名',
age INT(11) DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
manager_id BIGINT(20) DEFAULT NULL COMMENT '直属上级id',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
update_time DATETIME DEFAULT NULL COMMENT '修改时间',
version INT(11) DEFAULT '1' COMMENT '版本',
deleted INT(1) DEFAULT '0' COMMENT '逻辑删除标识(0.未删除,1.已删除)',
*/
//主键
private Long id;
//姓名
private String name;
//年龄
private Integer age;
//邮箱
private String email;
//直属上级
private Long managerId;
//创建时间
private LocalDateTime createTime;
//修改时间
private LocalDateTime updateTime;
//版本
private Integer version;
//逻辑删除标识(0.未删除 1.已删除)
//TODO 局部配置逻辑删除只是对应这个实体类而言,并不是全局的 delval逻辑已删除 value逻辑未删除
//@TableLogic(delval = "0",value = "0")
//如果是全局配置了的,声明此注解就行了,也可以不声明
@TableLogic
private Integer deleted;
}
3.config配置类
此项配置是与各位使用的mp的版本有关,如果你使用的是3.3.1以下的那你就需要创建一个配置类在配置类中注入一个ISqlInjector对象。如果mp的版本是3.3.1或者更高就无需进行此项配置
com.czxy.config.MyBatisConfig
package com.czxy.config;
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.injector.LogicSqlInjector;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
* @Author ScholarTang
* @Date 2019/10/31 7:56 PM
* @Desc mybatis-plus的配置类
*/
@Component
public class MyBatisPlusConfig {
/**
* 在这个配置类中注入LogicSqlInjector bean对象的原因是:
* 若mp的版本是3.3.1以下的就需要在此处注入这个bean对象
* 若mp的版本高于3.3.1的就无需在此处注入bean对象了
* 我使用的mp的版本是3.1.2,不需在此处注入这个bean对象
* @return
*/
@Bean
private ISqlInjector sqlInjector () {
return new LogicSqlInjector();
}
}
1.4逻辑删除的测试(code)
@Autowired
private UserMapper mapper;
//TODO 使用逻辑删除 删除表中一条记录
/**
* 运行测试类查看SQL语句:
* SQL:UPDATE user SET deleted=1 WHERE id=? AND deleted=0
* 会发现这个这条语句并没有使用delete去删除而是使用update去修改了者条记录中的用来表示逻辑删除的字段值
*/
@Test
void logicalDeletion() {
int number = mapper.deleteById(1094592041087729666L);
log.info("影响条数:"+number);
}
//TODO 在使用逻辑删除了一条记录后,查询所有记录看是否能查到刚刚逻辑被删除的记录
/**
* 运行测试类查看SQL语句:
* SQL:SELECT id,name,age,email,manager_id,create_time,update_time,version,deleted FROM user WHERE deleted=0
* 可以很清楚的看到它查询的所有记录都是deleted=0的记录(deleted=0 即为逻辑未删除),
* 所以刚刚使用逻辑删除的那条记录并没有被查出来,那修改当,批量修改都是这一个意思,都是对deleted=0的记录进行操作
*/
@Test
void selectAll() {
List<User> users = mapper.selectList(null);
log.info("users:{}",users);
}
1.5查询中排除删除表识字段即注意事项
SQL:SELECT id,name,age,email,manager_id,create_time,update_time,version,deleted FROM user WHERE deleted=0
在刚刚使用逻辑删除的操作删除了表中的一条记录后,查询了所有的记录,在查询语句中可以看到它加了一个条件,如果说不想看到这个where条件应该怎么实现?
实现方式:我们可以在实体类中的表示逻辑删除状态的属性上添加@TableField注解在注解中使select属性的值为false即可实现
@TableField(select = false)
private Integer deleted;
注意事项:
1.刚刚所描述的那种方法并不能适用于自定义SQL查询中,请看以下的列子:
我在UserMapper中自定义了一个SQL查询方法,然后在测试类中调用了我自定义的查询方法,查询数据,
这种方式它并不会自动给添加限定条件,也就是说它会将逻辑已删除的记录也查询出来,这时候我们可以自己去定义一下这个条件
com.czxy.mapper.UserMapper.mySelectAll
@Select("select * from user ${ew.customSqlSegment}")
List<User> mySelectAll (@Param(Constants.WRAPPER) Wrapper<User> wrapper);
测试
/**
* SQL:select * from user WHERE version = ? AND deleted = ?
*/
@Test
void mySelectAll () {
List<User> users = mapper.mySelectAll(
new LambdaQueryWrapper<User>()
.eq(User::getVersion, 1)
//添加限定条件
.eq(User::getDeleted,0));
log.info("users:{}",users);
}
2.自动填充
2.1.自动填充的简介:
再有的项目中,有新增时间,修改时间,新增人,修改人等字段。
一个比较笨的方法就是在新增时间的时候new Date(),修改时也是同样的一个套路,这样下来的化反复的操作几遍就会显得非常的繁琐。
在这种情况下我们可以使用mp提供的自动填充功能设置新增时间和更新时间
2.2.自动填充的实现
1.首先我们要在表对应的实体类的属性上添加@TableField注解,根据是插入时或者更新选择自动填充
2.在项目的目录下创建一个填充处理器实现mp提供的原对象处理器接口,实现自动填充
3.编写测试类,测试结果
1.@TableField注解的fill属性的属性值
/**
* 默认不处理
*/
DEFAULT,
/**
* 插入时填充字段
*/
INSERT,
/**
* 更新时填充字段
*/
UPDATE,
/**
* 插入和更新时填充字段
*/
INSERT_UPDATE
我要实现的是插入时和更新时自动填充时间,所以我在表对应的实体类对象中的属性上添加
com.czxy.domain.User
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
//修改时间
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
2.在项目目录下创建一个包,在包中编写一个类实现mp提供的原对象处理器接口,并在类中实现插入与更新时自动填充的方法
com.czxy.component
package com.czxy.component;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* @Author ScholarTang
* @Date 2019/10/31 11:11 PM
* @Desc 填充处理器实现mp提供的原对象处理器接口
*/
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
//TODO 插入时自动填充时间
/**
* 该方法有三个参数
* fieldName 属性名
* fieldVal 自动填充的值
* metaObject 原对象参数
*/
log.info("insert is ok");
//通用方法
//setFieldValByName() 无论是插入还是更新都可以调用
setInsertFieldValByName("createTime",LocalDateTime.now(),metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
//TODO 更新是自动填充时间
/**
* 该方法有三个参数
* fieldName 属性名
* fieldVal 自动填充的值
* metaObject 原对象参数
*/
log.info("update is ok");
//通用方法
//setFieldValByName() 无论是插入还是更新都可以调用
setUpdateFieldValByName("updateTime",LocalDateTime.now(),metaObject);
}
}
3.编写测试类,测试
//TODO 自动填充测试 添加
/**
* 运行结果:
* SQL:INSERT INTO user ( id, name, age, email, manager_id, create_time, version, deleted ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
* 数据:1087982257332887778(Long), 朱茵(String), 18(Integer), zy@nslm.com(String), 1087982257332887553(Long), 2019-10-31T23:30:56.350(LocalDateTime), 1(Integer), 0(Integer)
*
* 结果分析:
* look!look! 2019-10-31T23:30:56.350(LocalDateTime) 这就自动填充了
*/
@Test
void insertFill () {
//创建一个User对象,并将这个User对象插入表中的记录中
User user = new User(1087982257332887978L,"朱茵",18,"zy@nslm.com",1087982257332887553L,null,null,1,0);
int rows = mapper.insert(user);
System.out.println(rows);
log.info("影响条数:"+rows);
}
//TODO 修改记录时自动填充修改时间
/**
* 运行结果:
* SQL:UPDATE user SET age=?, update_time=? WHERE id=?
* 数据:19(Integer), 2019-10-31T23:38:52.427(LocalDateTime), 1087982257332887978(Long)
*
* 结果分析:
* 2019-10-31T23:38:52.427(LocalDateTime) 将当前时间做为修改的时间自动填充了,修改成功,填充成功
*/
@Test
void updateFill () {
//创建一个User对象,在这个对象中传入主键,和要修改的字段值,并修改
User user = new User();
user.setId(1087982257332887978L);
user.setAge(19);
int rows = mapper.updateById(user);
log.info("影响条数:"+rows);
}
2.3.自动填充的优化
1.插入数据时自动填充的优化
比方说,在我们插入的时候对插入的时间进行自动填充,那如果我表中的字段并没有这记录插入时间的字段,那岂不是浪费了资源,别担心继续往下看有办法解决!
解决方案:我们可以在填充处理器中进行判断,判断表中对应的实体对象中是否有这个属性,如果有就填充,没有就不填充
代码:com.czxy.component.MyMetaObjectHandler.insertFill
@Override
public void insertFill(MetaObject metaObject) {
//判断要插入的记录是否存在给定的字段名,返回布尔值 为true就是有,为false就是没有
//如果存在就填充,如果不存在就不填充
//我数据库表中是有createTime这个字段的,我改成其他的,测试结果
boolean hasGetter = metaObject.hasGetter("createTime1");
if (hasGetter) {
//TODO 插入时自动填充时间
/**
* 该方法有三个参数
* fieldName 属性名
* fieldVal 自动填充的值
* metaObject 原对象参数
*/
log.info("insert is ok");
setInsertFieldValByName("createTime",LocalDateTime.now(),metaObject);
}
}
代码:测试
//TODO 测试自动填充优化 (插入)
/**
* 运行结果一:
* SQL:INSERT INTO user ( id, name, age, email, manager_id, create_time, version, deleted ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
* 数据:1087982258332887978(Long), 朱茵(String), 18(Integer), zy@nslm.com(String), 1087982257332887553(Long), null, 1(Integer), 0(Integer)
*
* 结果分析:
* 从SQL语句和数据可以看出来并没有自动填充了
*
* 运行结果二:(我又将填充处理器中的字段名该回我表中的字段名进行测试)
* SQL:INSERT INTO user ( id, name, age, email, manager_id, create_time, version, deleted ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
* 数据:1187982258332887978(Long), 朱茵(String), 18(Integer), zy@nslm.com(String), 1087982257332887553(Long), 2019-11-01T00:10:31.083(LocalDateTime), 1(Integer), 0(Integer)
*
* 分析结果:
* 2019-11-01T00:10:31.083(LocalDateTime) 显然它自动帮我填充了时间
*
*/
@Test
void autoFillOptimization01 () {
User user = new User(1187982258332887978L,"朱茵",18,"zy@nslm.com",1087982257332887553L,null,null,1,0);
int rows = mapper.insert(user);
log.info("影响条数:"+rows);
}
2.更新数据时自动填充的优化
比如说:如果我在更新记录的时候自己设置了更新时间,我就不想让填充处理器帮我自动填充了,我没有设置的时候又帮我填充,其实也是在填充处理器中对应的更新方法上稍作改动,我们只需要判断它是否有被设置过值就OK,如果设置了就不执行自动填充,如果没有设置就执行自动填充
代码:com.czxy.component.MyMetaObjectHandler.updateFill
@Override
public void updateFill(MetaObject metaObject) {
//这个方法就是判断这个指定的字段是否被设置了值,如果被设置了子这个对象就不为null,就不需要自动填充了
//否则就是为null,为null时就自动填充
Object val = getFieldValByName("updateTime", metaObject);
if (val == null) {
//TODO 更新是自动填充时间
/**
* 该方法有三个参数
* fieldName 属性名
* fieldVal 自动填充的值
* metaObject 原对象参数
*/
log.info("update is ok");
setUpdateFieldValByName("updateTime",LocalDateTime.now(),metaObject);
}
}
代码:测试
//TODO 自动填充优化 (更新)
/**
* 运行结果一:(更新记录时自己设置了更新时间)
* SQL:UPDATE user SET age=?, update_time=? WHERE id=?
* 数据:19(Integer), 2019-11-01T00:20:49.671(LocalDateTime), 1187982258332887978(Long)
*
* 结果分析:
* 从SQL和数据是看不出来是否则是填充的,我在更新时自动填充的方法中打印了一句话,在这里控制台并没有打印,所以
* 测试结果是成功的,它没有走填充处理器中的方法
*
*
* 运行结果二:(更新记录时自己没有设置更新时间)
* SQL:UPDATE user SET age=?, update_time=? WHERE id=?
* 数据:19(Integer), 2019-11-01T00:23:35.055(LocalDateTime), 1187982258332887978(Long)
*
* 结果分析:
* 和运行结果一的道理一样,控制台有输出我打印的语句,测试是成功的
*/
@Test
void autoFillOptimization02 () {
User user = new User();
user.setId(1187982258332887978L);
user.setAge(19);
// user.setUpdateTime(LocalDateTime.now());
int rows = mapper.updateById(user);
log.info("影响条数:"+rows);
}
3.乐观锁插件
3.1.乐观锁的简介
乐观锁的意图:当要更新一条记录时,希望这条记录没有被别人更新过,防止更新冲突。
使用乐观锁实现的方式一:取出数据时获取当前的version(版本),更新时,带上这个version,版本正确更新成功,错误更新失败
为什么要使用乐观锁:
关于数据库的锁分为悲观锁和乐观锁,悲观锁是通过数据库的锁机制实现的,这两种锁各有优缺,不是说谁好于谁。
乐观锁和悲观锁的应用场景:
适用于写比较少的情况下,即为多度场景。在多读场景即使有冲突也很少发生,这样可以省去了锁的开销,加大了系统整体吞吐量,如果是多写的情况下,一般会经常产生冲突上传的应用会不断的重试,降低了系统的性能,所以多写的情况下还是使用悲观锁。
3.2乐观锁的实现
3.2.1.实现步骤
1.在配置类中配置乐观锁插件
在实体类中找到数据库表中表示版本号的字段名在实体对象中对应的属性名version,并在这个属性上添加一个@Version注解
配置类:com.czxy.config.MyBatisPlusConfig
/**
* 配置乐观锁插件
* @return
*/
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor (){
return new OptimisticLockerInterceptor();
}
表实体对象User:com.czxy.domian.User
//版本
@Version
private Integer version;
测试
//TODO 乐观锁实现更新(修改)记录,改变表字段版本
/**
* 运行结果:
* SQL:UPDATE user SET age=?, email=?, update_time=?, version=? WHERE id=? AND version=?
* 数据:20(Integer), zy1@nslm.com(String), 2019-11-01T08:48:35.115(LocalDateTime), 2(Integer), 1187982258332887978(Long), 1(Integer)
*
* 结果分析:
* 显然是更新记录成功了,并且它将当前的版本号等于旧的版本号加一了
*/
@Test
void optimisticLock01 () {
//一般情况下都是去数据库中查取记录的version,但是为了方便测试
//我直接在这定义变量表示当前记录在数据库表中的version
Integer version = 1;
//创建一个User类,在User类对象中封装记录ID,以及要修改的字段,此条记录当前的版本号
User user = new User();
user.setId(1187982258332887978L);
user.setAge(20);
user.setVersion(version);
user.setEmail("zy1@nslm.com");
int rows = mapper.updateById(user);
log.info("影响条数:"+rows);
}
3.2.2.注意事项(此处说明来自mp官方文档)
@Version
private Integer version;
特别说明:
支持的数据类型只有:int,Integer,long,Login,Date,Timestamo,LocaIDateTime
整数类型下:newVersion = oldVerSion +1
newVersion 会回写到entity中
仅支持updateById(id) 与 update(entity,wrapper)方法
在update(entity,wrapper)方法下wrapper不能复用!
演示update(entity,wrapper)方法中wrapper被复用
//TODO 更新冲突(修改冲突)
/**
* 先执行的修改方法的运行结果:
* SQL:
* SQL: UPDATE user SET email=?, update_time=?, version=? WHERE name = ? AND age = ? AND version = ?
* 数据:12736902@123.com(String), 2019-11-01T09:11:35.526(LocalDateTime), 4(Integer), 王天风(String), 25(Integer), 3(Integer)
* 结果分析:
* 这是更新成功的,返回的影响条数为一,并且将当前的版本好改成了旧的版本号加一
*
* 复用了第一个修改方法的条件构造器的修改方法运行结果:
* SQL:UPDATE user SET email=?, update_time=?, version=? WHERE name = ? AND age = ? AND version = ? AND name = ? AND age = ? AND version = ?
* 数据:12736902@123.com(String), 2019-11-01T09:11:35.526(LocalDateTime), 5(Integer), 王天风(String), 25(Integer), 3(Integer), 王天风(String), 25(Integer), 4(Integer)
* 结果分析:
* 可以看到SQL语句中的version = ? AND name = ? AND age = ? AND version = ?,同时判断了两个版本号,所以这条SQL是执行不成功的
* 返回的影响条数为0
*
*/
@Test
void optimisticLock02 () {
//这个测试方法演示的是乐观锁的注意事项之在update(entity,wrapper)方法下wrapper不能复用
//实现方式:
//1.创建一个User对象,在User对象中传入对应的主键,版本号,和要修改的字段值
//2.创建条件构造器设置更新条件
//3.mapper调用方法修改记录
//4.再创建一个User对象,在User对象中传入对应的主键,版本号(这个版本号即为上一条记录的版本好加一)和要修改的字段值
//5.mapper调用方法修改记录
User u01 = new User();
u01.setId(1088248166370832385L);
//方便测直接设置版本号
u01.setVersion(3);
u01.setEmail("12736902@123.com");
QueryWrapper<User> wrapper = Wrappers.query();
wrapper.eq("name","王天风").eq("age",25);
int rows01 = mapper.update(u01, wrapper);
log.info("第一个更新方法的影响条数:"+rows01);
User u02 = new User();
u02.setVersion(4);
u02.setEmail("wtf@baomidou.com");
wrapper.eq("name","王天风").eq("age",25);
int rows02 = mapper.update(u01, wrapper);
log.info("第二个更新方法的影响条数:"+rows02);
}