背景:项目中使用Mybatis做持久层,数据库表设计上存在很多表存在共同的字段,比如创建/更新者、创建/修建时间字段。
做法:对于表映射的实体类,可以将相同的字段抽离到父类(抽象或者普通类),但需要维护这些字段数据的插入/修改。
问题:如何更新/维护这些共同的字段?如果一次操作涉及到多张存在相同字段的表又该如何做?
DEMO技术栈:SpringBoot + Mybatis + H2 + SpringSecurity
- SpringBoot 主要利用spring-boot-starter-web来实现测试接口
- Mybatis 项目用到的持久层方案,本文主要讨论以Mybatis基于Executor类型的拦截原理展开(纯注解版)
- H2 用于测试的内存数据库
- SpringSecurity 用于通过spring security上下文获取登陆人的id,本文不是以spring security为主,简单集成
思路分析与讨论
- 对于数据库表跟Dao/Mapper来讲是一一对应的;Service层面的一次操作,如果涉及到多张存在相同字段的表操作,需要一次性同时更新多张表的更新字段(人/时间)
- 直接在Service做AOP拦截的,是基于整个方法的拦截,达不到维护相同字段的需求
- 直接在mapper(xml或注解中维护)的方式,不得不自己写代码维护,维护相同字段的逻辑会遍布你的代码/xml当中,增大了代码维护度;即便这么做也很难做到通用化。
- 回归标题:只能回到Mybatis的底层基于插件机制的拦截(分页插件也是基于此原理,只是拦截类型不一样;其他方案还有MybatisPlus)
走起!贴代码!!(gradle方式)
- 项目依赖(SpringBoot/Security2.6.1+Mybatis2.2.2)
buildscript {
ext {
springBootVersion = '2.6.1'
}
repositories {
maven { url 'https://maven.aliyun.com/repository/public/'}
mavenLocal()
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'//spring-boot-starter依赖管理
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies{
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-security"
implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2")
runtimeOnly "com.h2database:h2"
}
repositories {
maven { url 'https://maven.aliyun.com/repository/public/'}
mavenLocal()
mavenCentral()
}
- 配置应用端口、应用上下文、初始化H2 ddl文件、Mybatis字段驼峰映射
server:
port: 8888
servlet.context-path: /demo
spring:
datasource:
schema: classpath:ddl.sql
mybatis:
configuration:
map-underscore-to-camel-case: true
create table primary_entity ( id integer primary key auto_increment,name varchar,create_time timestamp,created_by varchar,update_time timestamp,updated_by varchar);
insert into primary_entity(name,create_time,created_by,update_time,updated_by)values('hello demo name',CURRENT_TIMESTAMP(),'admin',CURRENT_TIMESTAMP(),'admin');
create table secondary_entity (id integer primary key auto_increment,desc varchar,create_time timestamp,created_by varchar,update_time timestamp,updated_by varchar);
insert into secondary_entity(desc,create_time,created_by,update_time,updated_by)values('hello demo desc',CURRENT_TIMESTAMP(),'admin',CURRENT_TIMESTAMP(),'admin');
* 定义模型实体类BasicEntity、PrimaryEntity、SecondaryEntity(BasicEntity包含公共维护字段,PrimaryEntity、SecondaryEntity继承BasicEntity)
- SpringSecurity简单配置,写死一个账号,只是为了模拟维护createdBy/updatedBy公共字段时,从SecurityContext获取登录用户id
public class BasicEntity {
private String createdBy;
private String updatedBy;
private Date createTime;
private Date updateTime;
}
public class PrimaryEntity extends BasicEntity {
private Integer id;
private String name;
}
public class SecondaryEntity extends BasicEntity {
private Integer id;
private String desc;
}
- 定义DAO/Mapper层,PrimaryMapper、SecondaryMapper
@Mapper
public interface PrimaryMapper {
@Insert({"insert into primary_entity(name)values(#{name})"})
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(PrimaryEntity primaryEntity);
@Select("select * from primary_entity where primary_entity.id=#{id}")
PrimaryEntity find(@Param("id") Integer id);
@Update("update primary_entity set name=#{name} where primary_entity.id=#{id}")
int update(PrimaryEntity primaryEntity);
}
@Mapper
public interface SecondaryMapper {
@Insert({"insert into secondary_entity(desc)values(#{desc})"})
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(SecondaryEntity secondaryEntity);
@Select("select * from secondary_entity where secondary_entity.id=#{id}")
SecondaryEntity find(@Param("id") Integer id);
@Update("update secondary_entity set desc=#{desc} where secondary_entity.id=#{id}")
int update(SecondaryEntity secondaryEntity);
}
public class BothEntitiesVo {
private PrimaryEntity primaryEntity;
private SecondaryEntity secondaryEntity;
}
public interface DemoService {
//向PrimaryEntity、SecondaryEntity表插入数据,content分别存储于PrimaryEntity的name字段、SecondaryEntity的desc字段
//返回 包含PrimaryEntity和SecondaryEntity的BothEntitiesVo (测试新增数据后,createdBy,createTime的值是否插入数据库)
BothEntitiesVo insert(String content);
//返回 包含PrimaryEntity和SecondaryEntity的BothEntitiesVo (测试修改数据后,updatedBy,updateTime的值是否插入数据库)
BothEntitiesVo update(Integer pid,Integer sid,String content);
}
@Service
public class DemoServiceImpl implements DemoService{
@Autowired
PrimaryMapper primaryMapper;
@Autowired
SecondaryMapper secondaryMapper;
@Override
public BothEntitiesVo insert(String content) {
PrimaryEntity primaryEntity = new PrimaryEntity();
primaryEntity.setName("NAME==".concat(content));//将content存入PrimaryEntity的name字段
primaryMapper.insert(primaryEntity);
SecondaryEntity secondaryEntity = new SecondaryEntity();
secondaryEntity.setDesc("DESC=".concat(content));//将content存入SecondaryEntity的desc字段
secondaryMapper.insert(secondaryEntity);
return new BothEntitiesVo(primaryEntity,secondaryEntity);
}
@Override
public BothEntitiesVo update(Integer pid,Integer sid,String content) {
PrimaryEntity primaryEntity = primaryMapper.find(pid);
primaryEntity.setName("name=".concat(content));//修改PrimaryEntity的name字段为content
primaryMapper.update(primaryEntity);
SecondaryEntity secondaryEntity = secondaryMapper.find(sid);
secondaryEntity.setDesc("desc=".concat(content));//修改SecondaryEntity的desc字段为content
secondaryMapper.update(secondaryEntity);
return new BothEntitiesVo(primaryEntity,secondaryEntity);
}
}
- 【核心】定义拦截类CommonInterceptor,再对数据库操作前,对实体表的公共字段进行维护
@Component
@Intercepts({
@Signature(type=Executor.class,method="update",args={MappedStatement.class,Object.class}),
})
public class CommonInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0];
Object object = invocation.getArgs()[1];
if(object instanceof BasicEntity){
BasicEntity basiceEntity = (BasicEntity)object;
Date date = new Date();
//从SpringSecurity上下文获取登录者
String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
//如果是插入数据,更新创建者与创建时间
if(mappedStatement.getSqlCommandType() == SqlCommandType.INSERT){
basiceEntity.setCreatedBy(currentUser);
basiceEntity.setCreateTime(date);
}
// 更新修改者与修改时间
basiceEntity.setUpdatedBy(currentUser);
basiceEntity.setUpdateTime(date);
}
return invocation.proceed();
}
}
@RestController
public class DemoController {
@Autowired
DemoService demoService;
@GetMapping("/insert/{content}")
public BothEntitiesVo insert(@PathVariable(value = "content")String content){
return demoService.insert(Objects.isNull(content)?"TEST":content);
}
@GetMapping("/update/{pid}/{sid}/{content}")
public BothEntitiesVo update(
@PathVariable(value = "pid")Integer pid,
@PathVariable(value = "sid")Integer sid,
@PathVariable(value = "content")String content
){
return demoService.update(pid,sid,Objects.isNull(content)?"TEST":content);
}
}
- 测试,直接更新ddl文件中创建的PrimaryEntity(id=1)和SecondaryEntity(id=1)
修改测试
浏览器访问 http://127.0.0.1:8888/demo/update/1/1/efg
使用john/123456 进行表单登录
结果如下(name/desc被修改成带efg,注意 创建者是ddl写死的admin,修改人是john):
{"primaryEntity":{"createdBy":"admin","updatedBy":"john","createTime":"2022-03-13T13:02:49.737+00:00","updateTime":"2022-03-13T13:03:28.083+00:00","id":1,"name":"name=efg"},"secondaryEntity":{"createdBy":"admin","updatedBy":"john","createTime":"2022-03-13T13:02:49.762+00:00","updateTime":"2022-03-13T13:03:28.090+00:00","id":1,"desc":"desc=efg"}}
新增测试
浏览器访问 http://127.0.0.1:8888/demo/insert/abcefghijklmnopqrstuvwxyz
使用john/123456 进行表单登录(如果登陆过了,忽略)
结果如下(name/desc被修改成带abcefghijklmnopqrstuvwxyz,注意创建者和修改人都是john):
{"primaryEntity":{"createdBy":"john","updatedBy":"john","createTime":"2022-03-13T13:06:15.673+00:00","updateTime":"2022-03-13T13:06:15.673+00:00","id":2,"name":"NAME==abcefghijklmnopqrstuvwxyz"},"secondaryEntity":{"createdBy":"john","updatedBy":"john","createTime":"2022-03-13T13:06:15.679+00:00","updateTime":"2022-03-13T13:06:15.679+00:00","id":2,"desc":"DESC=abcefghijklmnopqrstuvwxyz"}}