简单的说,乐观说就是一个修改记录版本记录。每次提交修改时都要提交这个版本号,数据库里用来复核对不行。相当于是这样:
update tb set count =new,version = version+1 count where version= version
每次更新时,version都会更新,比如自动=1.这样下次再更新时,由于version已经变了就不会被更新了。这样可以确认每一次的更新都能在对的版本上,如果版本不对,就需要重新获取数据。
mybatisPlus 的自带了乐观锁的功能,一般使用int 类型的version当版本号,或者用updateTime来当版本号。
两者适用的场景不同,各有优劣。下面我以updatedAt当锁来讲解如何配置。
1、配置类注入mybatisPlus乐观锁的拦截器
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//1 创建MybatisPlusInterceptor拦截器对象
MybatisPlusInterceptor mpInterceptor=new MybatisPlusInterceptor();
//2 添加乐观锁拦截器
mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mpInterceptor;
}
}
2、定义一个自动填充行为,用来强制将当前AutoFillFieldValueConfig。
在updateFill中将updatedAt当前锁,强制更新当前时间。
package com.luo.comm.config.mybatisPlus;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.luo.comm.entity.User;
import com.luo.comm.utils.ThreadLocal.ThreadlUser;
import com.luo.comm.vo.BusinessException;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 自动填充字段值的配置
* 插入时,保存创建时间和创建人,以及更新时间和更新人。
* 更新时,保存更新时间和更新人。
* 其中,创建人,更新人都是从ThreadlUser获取的当前线程用户信息
*/
@Component
public class AutoFillFieldValueConfig implements MetaObjectHandler {
private static final String createdBy = "createdBy";
private static final String updatedBy = "updatedBy";
private static final String createdAt = "createdAt";
private static final String updatedAt = "updatedAt";
@Override
public void insertFill(MetaObject metaObject) {
User loginUser = getLoginUser();
this.strictInsertFill(metaObject, createdAt, Date.class, new Date());
this.strictInsertFill(metaObject, createdBy, String.class, loginUser.getUserName());
this.strictInsertFill(metaObject, updatedAt, Date.class, new Date());
this.strictInsertFill(metaObject, updatedBy, String.class, loginUser.getUserName());
}
@Override
public void updateFill(MetaObject metaObject) {
User loginUser = getLoginUser();
this.strictInsertFill(metaObject, updatedAt, Date.class, new Date());
this.strictInsertFill(metaObject, updatedBy, String.class, loginUser.getUserName());
}
private User getLoginUser (){
/** 获取当前线程的用户信息,检查是否登陆 */
User loginUser = ThreadlUser.read();
System.out.println("当前线程1:"+ Thread.currentThread().getId() +"user:" +loginUser);
if(StringUtils.isEmpty(loginUser.getUserName()) || StringUtils.isEmpty(loginUser.getRoleId())){
throw BusinessException.error ("用户未登陆或不存在");
}
return loginUser;
}
}
3、在实体中,通过@version注解标识锁字段
添加了@Version以后,mybatisPlus会自动识别。
/** 更新时间*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
@JsonInclude(JsonInclude.Include.NON_NULL) // 当为空时不转换JSON输出。这样前端就不会返回null.
@TableField(fill = FieldFill.INSERT_UPDATE)
@Version
private Date updatedAt;
4、在通用update方法中,beforeUpdateComm中调用一个通用处理方法,用来确保所以有update方法都会得到执行beforeUpdateCommHandle。
public final int update(T entity){
// 将原变量的属性复制一份到新实体类。后面都用新实体类处理。
// T newEntity =entity;
beforeUpdate(entity);
beforeUpdateComm(entity);
int i = baseMapper.updateById(entity);
return i;
};
protected void beforeUpdate(T entity){
};
private void beforeUpdateComm(T entity){
// 调用保存方法前处理过程,进行一些通用字段的处理或通用条件的处理
MpUtils.beforeUpdateCommHandle(entity);
}
package com.luo.comm.services.mp;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luo.comm.utils.mp.MpUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 通用基类,定义了通用的方法.Service中都是封装的方法,如果要用原生的方法,请直接使用UserMapper的方法。
* public方法是最终对外暴露的方法,不建议子类重写特意加了final关键字禁止子类重写。
* save方法,内部调用了beforeSave、beforeSaveComm、afterSaveComm、afterSave。Comm结尾的是公共的,不能被子类重写。
* 同理,增删改查方法内部都会调用一些处理过程。Comm结尾的是公共的方法,不能被子类重写。
*
* protected 方法 能被子类继承和重写。
* private 方法 仅能在本类中调用,不能被子类继承,也不能被子类重写。
*
* @author bill
* @date 2023-07-07
* */
@Component
public class BaseService<T> {
@Resource
BaseMapper<T> baseMapper;
public final int save(T entity){
// 将原变量的属性复制一份到新实体类。后面都用新实体类处理。
// T newEntity =entity;
beforeSave(entity);
beforeSaveComm(entity);
int insert = baseMapper.insert(entity);
return insert;
};
protected void beforeSave(T entity){
};
private void beforeSaveComm(T entity){
// 调用保存方法前处理过程,进行一些通用字段的处理或通用条件的处理
MpUtils.beforeSaveCommHandle(entity);
}
/**
* 这是一个单条修改的方法。
* 采用乐观锁更新机制,锁不是version,是updateAt.精确到豪米。所以每次更新时都要携带updateAt字段
*
* */
public final int update(T entity){
// 将原变量的属性复制一份到新实体类。后面都用新实体类处理。
// T newEntity =entity;
beforeUpdate(entity);
beforeUpdateComm(entity);
int i = baseMapper.updateById(entity);
return i;
};
protected void beforeUpdate(T entity){
};
private void beforeUpdateComm(T entity){
// 调用保存方法前处理过程,进行一些通用字段的处理或通用条件的处理
MpUtils.beforeUpdateCommHandle(entity);
}
public final long count(QueryWrapper queryWrapper){
return baseMapper.selectCount(queryWrapper);
};
}
5、实现类中,只需要继承即可。
public class UserService extends BaseService<User> {}
6、beforeUpdateCommHandle中进行处理,检查一定要有updatedAt字段。
package com.luo.comm.utils.mp;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.luo.comm.vo.BusinessException;
import com.luo.comm.vo.MyResEnum;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import java.util.Arrays;
import java.util.List;
public class MpUtils {
/**
* 保存前的预处理函数,像删除id\isDelete字段等
* @param object 入参是一个通用实体类,转换成jsonobject进行处理*/
private static final String id = "id";
private static final String createdBy = "createdBy";
private static final String updatedBy = "updatedBy";
private static final String createdAt = "createdAt";
private static final String updatedAt = "updatedAt";
private static final String deletedBy = "deletedBy";
private static final String deletedAt = "deletedAt";
public static Object beforeSaveCommHandle(Object object) {
Object obj = object;
/** 将baseEntity的属性补充到当前类,供后面使用 */
Class<?> cls = obj.getClass();
JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(obj));
/** 处理需要删除的字段。这些是不能指定添加的 */
// removeList列表中字段都会被删除处理。约定这些是特殊字段,不能由用户填写
List<String> removeList = Arrays.asList(id,createdBy,updatedBy,createdAt,updatedAt,deletedBy,deletedAt);
for (String item: removeList ) {
if(jsonObject.containsKey(item)){
jsonObject.remove(item);
}
}
Object newObj = jsonObject.toJavaObject(cls);
// 处理结束后,将新实体类的属性再复制回原实体类,以便调用者可以直接用实体类获取新值。
BeanUtils.copyProperties(newObj,object);
/**是否返回object并不影响调用者通过object获取新值,这里也可以返回int或bool值*/
return object;
}
/**
* 保存前的预处理函数,删除createdBy,createdAt,deletedBy,deletedAt字段等
* @param object 入参是一个通用实体类,转换成jsonobject进行处理*/
public static Object beforeUpdateCommHandle(Object object) {
Object obj = object;
/** 将baseEntity的属性补充到当前类,供后面使用 */
Class<?> cls = obj.getClass();
JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(obj));
/**检查是否存在id字段。不存在抛异常*/
if(!jsonObject.containsKey(id) || ObjectUtils.isEmpty(jsonObject.get(id))){
throw new BusinessException(MyResEnum.ID_EMPTY); // update方法调用的其实是updatedById,所以必须提供id。
}
/**检查是否存在乐观锁updateAt字段。不存在抛异常*/
if(!jsonObject.containsKey(updatedAt) || ObjectUtils.isEmpty(jsonObject.get(updatedAt))){
throw BusinessException.error("更新时必须提供updatedAt字段");
}
/** 处理需要删除的字段。这些是不能指定的 */
// removeList列表中字段都会被删除处理。
List<String> removeList = Arrays.asList(createdBy,createdAt,deletedBy,deletedAt);
for (String item: removeList ) {
if(jsonObject.containsKey(item)){
jsonObject.remove(item);
}
}
/** 删除公用字段外,除id和updatedAt字段以外,还需要包括至少一个字段,否则无更新内容*/
if(jsonObject.keySet().stream().count()<3){
throw new BusinessException(MyResEnum.UPDATE_NO_FIELDS);
}
Object newObj = jsonObject.toJavaObject(cls);
// 处理结束后,将新实体类的属性再复制回原实体类,以便调用者可以直接用实体类获取新值。
BeanUtils.copyProperties(newObj,object);
/**是否返回object并不影响调用者通过object获取新值,这里也可以返回int或bool值*/
return object;
}
}
7、controller调用示例
@PostMapping("/update")
public ResultsObj update(@RequestBody User user) {
int i = userService.update(user);
ResultsObj resultsObj =i>0 ?new ResultsObj(user):new ResultsObj(MyResEnum.UPDATE_FAIL);
return resultsObj;
}
8、postman调用示例
如果不提供正确的updatedAt字段,都会更新失败。
下面提供正确的,返回成功:
9、本项目gitee代码仓地址
截止到目前,已经集成了mybatisPlus并进行了通用的save\update方法的封装。
为便于直接使用,本项目的代码分享到gitee了,可直接下载。
laoluo: Springboot、SpringCloud项目
本项目代码位置于上述代码仓项目中services-backend项目。
本项目代码后面也会持续更新,并添加更多项目。