之前分享过关于Mybatis-Plus的模块集成和代码分层,文本分享关于Mybatis-Plus的单表操作和分表查询。
Mybatis-Plus对于单表提供了很强大的CRUD功能,核心主要还是依赖于Entity和Mapper,通过定义Entity和Mapper,Mybatis-Plus便能获取到表信息TableInfo,有了表的基本信息后便可为所欲为。
以客户端信息统计查询为例,想要使用Mybatis-Plus,首先就是创建Entity和Mapper,出于扩展考虑,自定义XwMapper为统一接口。
public interface XwMapper<T> extends BaseMapper<T> {
}
通过IDEA的MybatisX插件,我们可以很方便的依赖数据库表生成相应的实体,MybatisX的具体使用这里就不赘述,网上有很多相应的资料。Mybatis-Plus对于查询语句可以支持Lambda 表达式,也可以直接写查询字段对应的属性,其中FIELDS是用来定义一些字符串类型的字段,以兼容没法使用Lambda 表达式的场景。
@TableName(value ="ipush_client_info")
public class IpushClientInfo implements Serializable {
@TableField(exist = false)
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId
private Long id;
/**
* 应用的主键
*/
private Long appId;
/**
* 设备注册id
*/
private String xwRegId;
public static class FIELDS {
public static final String BRAND = "brand";
public static final String APP_Id = "app_id";
public static final String CREATE_TIME = "create_time";
public static final String USER_COUNT = "user_count";
}
/**
* 省略其他一些字段和get/set方法
*/
@Override
public String toString() {
return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
定义IpushClientInfoMapper
,Mybatis-Plus提供单表的CRUD功能,具体可以查看官网文档:https://baomidou.com/pages/49cc81/。
@Mapper
public interface IpushClientInfoMapper extends XwMapper<UserInfo> {
}
继承XwWrapper
定义IpushClientInfoWrapper
,Wrapper中要求指定对应的Mapper和对应的Entity。Mybatis-Plus提供了Wrapper用于条件处理,支持使用QueryWrapper
直接传入操作的列名,也支持使用Lambda表达式,最后调用baseMapper
进行处理。
@Component
public class IpushClientInfoWrapper extends XwWrapper<IpushClientInfoMapper, IpushClientInfo> {
/**
* 使用QueryWrapper
* @param startTime
* @param endTime
* @return
*/
public List<IpushUserStatByDay> getIpushUserStatByDay(Date startTime, Date endTime){
QueryWrapper<IpushClientInfo> query = Wrappers.query();
query.select(
IpushClientInfo.FIELDS.APP_Id,
IpushClientInfo.FIELDS.BRAND
).between(IpushClientInfo.FIELDS.CREATE_TIME, startTime, endTime)
.groupBy(IpushClientInfo.FIELDS.APP_Id, IpushClientInfo.FIELDS.BRAND);
List<IpushClientInfo> clientInfos = baseMapper.selectList(query);
return clientInfos.stream().map(IpushUserStatByDay::build).collect(Collectors.toList());
}
/**
* 使用LambdaQueryWrapper
* @param startTime
* @param endTime
* @param appId
* @return
*/
public int getAddUserCount(Date startTime, Date endTime, Long appId){
LambdaQueryWrapper<IpushClientInfo> query = Wrappers.lambdaQuery();
query.eq(IpushClientInfo::getAppId, appId)
.between(IpushClientInfo::getCreateTime, startTime, endTime);
return Parser.parserInt(baseMapper.selectCount(query));
}
}
优先使用LambdaQueryWrapper操作,对于字段中存在函数的则使用QueryWrapper进行处理,对于结果则通过流式处理进行类型转换,数据类型转换的方法统一定义在DTO中,方法命名按照如下标准:
方法 | 含义 |
---|---|
transfer() | DTO转化成Entity |
build() | Entity转化成DTO |
对于分表的处理,可以通过Mybatis-Plus动态表名插件(DynamicTableNameInnerInterceptor
)来实现,实现的思路主要如下:
自定义注解@MPShardingAnno
来声明需要分表操作的方法,以及@Param
声明需要进行补充表名的后缀。在注解解析器中,获取当前的表后缀存放到ThreadLocal
中,在插件解析的时候从ThreadLocal
中获取表后缀进行表名替换,等处理完后再释放对应的资源,出于扩展的考虑,还支持SpEL表达式。
自定义注解:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface MPShardingAnno {
/**
* 获取分表日期的SpEL表达式
*/
String dateExp() default "";
/**
* 已经拼装好的分天表后缀。如果配置了dateExp,则自动忽略该配置
*/
String tableSuffix() default "";
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Param {
String value();
}
定义TableNameContext来维护表后缀的上下文关系。
public class TableNameContext {
private TableNameContext() {
}
/**
* 维护表后缀
*/
private static ThreadLocal<String> suffixTableLocal = new ThreadLocal<>();
/**
* 维护方法嵌套关系
*/
private static ThreadLocal<Integer> numberThreadLocal = ThreadLocal.withInitial(() -> 0);
/**
* 初始化表后缀
* @param tableSuffix
*/
public static void initSuffix(String tableSuffix) {
suffixTableLocal.set(tableSuffix);
}
/**
* 获取表后缀
* @return
*/
public static String getSuffix(){
return suffixTableLocal.get();
}
/**
* 记录当前进入声明了{@link MPShardingAnno}的方法数
*/
public static void addThreadNum() {
Integer number = numberThreadLocal.get();
number++;
numberThreadLocal.set(number);
}
/**
* 记录当前结束声明了{@link MPShardingAnno}的方法数
* @return
*/
public static Integer reduceThreadNum(){
Integer number = numberThreadLocal.get();
number--;
numberThreadLocal.set(number);
return number;
}
/**
* 释放资源
*/
public static void release(){
suffixTableLocal.remove();
numberThreadLocal.remove();
}
}
自定义拦截切面,其中代码的注释都说明得很清楚,这里就不再累述。
@Component
@Aspect
@Order(1)
public class MPShardingAspect {
@Pointcut("@annotation(com.keduw.common.mybatisplus.annotation.MPShardingAnno)")
public void pointcut() {
}
/**
* 从代理的参数中获取表表后缀
*/
@Before("pointcut()")
public void beforeExecute(JoinPoint joinPoint) throws NoSuchMethodException {
Object target = joinPoint.getTarget();
MethodSignature methdSignature = (MethodSignature) joinPoint.getSignature();
Method method = methdSignature.getMethod();
// 获取注解所在方法的参数定义
Parameter[] parameters = method.getParameters();
if (parameters == null || parameters.length == 0) {
parameters = new Parameter[0];
}
MPShardingAnno shardingAnno = method.getAnnotation(MPShardingAnno.class);
// 针对jdk代理做兼容
if (shardingAnno == null) {
Method declaredMethod = target.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
shardingAnno = declaredMethod.getAnnotation(MPShardingAnno.class);
parameters = declaredMethod.getParameters();
}
String dateExp = shardingAnno.dateExp();
// 获取spring的SpEL表达式解析器
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
// 获取注解所在方法的参数值
List<String> paramNameList = new ArrayList<>();
Object[] args = joinPoint.getArgs();
for (Parameter parameter : parameters) {
Param paramNameAnno = parameter.getAnnotation(Param.class);
if (paramNameAnno != null) {
String paramName = paramNameAnno.value();
paramNameList.add(paramName);
} else {
paramNameList.add(parameter.getName());
}
}
for (int i = 0; i < paramNameList.size(); i++) {
context.setVariable(paramNameList.get(i), args[i]);
}
// 获取分表后缀
String tableSuffix = "";
if (StringUtils.isNotBlank(dateExp)) {
tableSuffix = ShardingUtil.getTableSuffix(getDateFromExp(dateExp, parser, context));
} else {
tableSuffix = getTableSuffixFromExp(shardingAnno.tableSuffix(), parser, context);
}
// 记录表后缀
TableNameContext.initSuffix(tableSuffix);
// 记录调用的深度
TableNameContext.addThreadNum();
}
@After("pointcut()")
public void afterExecute() {
Integer number = TableNameContext.reduceThreadNum();
if(number <= 0){
TableNameContext.release();
}
}
/**
* 根据日期的表达式,获取日期值
*
* @param dateExp 日期表达式
* @param parser 表达式解析器
* @param context 自定义上下文
*/
private Date getDateFromExp(String dateExp, ExpressionParser parser, StandardEvaluationContext context) {
if (StringUtils.isBlank(dateExp)) {
throw new IllegalArgumentException("dateExp is empty");
}
Object dateObj = parser.parseExpression(dateExp).getValue(context);
if (dateObj == null) {
throw new IllegalArgumentException("dateExp is empty");
}
Date dateVal = null;
if (dateObj instanceof Date) {
dateVal = (Date) dateObj;
} else if (dateObj instanceof Long) {
dateVal = new Date((Long) dateObj);
} else if (dateObj.getClass().isPrimitive() && "long".equals(dateObj.getClass().getTypeName())) {
dateVal = new Date(Parser.parserLong(dateObj));
} else {
throw new IllegalArgumentException("dateExp support dataType:java.util.Date/java.lang.Long/java.lang.String/Long");
}
return dateVal;
}
private String getTableSuffixFromExp(String suffixExp, ExpressionParser parser, StandardEvaluationContext context) {
if (StringUtils.isBlank(suffixExp)) {
return "";
}
Object msgTypeObj = parser.parseExpression(suffixExp).getValue(context);
return msgTypeObj != null ? msgTypeObj.toString() : null;
}
}
最后在定义Mybatis-Plus插件中引入动态表名插件,进行表名替换。
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//动态表名插件
DynamicTableNameInnerInterceptor innerInterceptor = new DynamicTableNameInnerInterceptor();
innerInterceptor.setTableNameHandler((sql, tableName) -> {
String suffix = TableNameContext.getSuffix();
if(StringUtils.isNotBlank(suffix)){
tableName += suffix;
}
return tableName;
});
interceptor.addInnerInterceptor(innerInterceptor);
return interceptor;
}
注解使用Spring的Aspect切面来实现功能,所以需要启用Spring的Aspect切面支持,如果使用到了事务,需要在@Transactional注解的地方同时加上该注解。可以通过dateExp指定表后缀,也可以直接通过tableSuffix声明,具体使用如下:
@MPShardingAnno(dateExp = "#postTime")
public List<MsgFrame> getMsgFrameByCancel(long packId, @Param("postTime") Date postTime) {
}
@MPShardingAnno(dateExp = "#schedulePack.postTime")
public List<IccMsgFrame> findFrameByPackId(@Param("schedulePack") SchedulePack pack) {
}
@MPShardingAnno(tableSuffix = "#suffix")
public List<ContactsTerminal> getContactsTerminalByCode(String code, @Param("suffix") String tableSuffix) {
}
文章主要分享关于Mybatis-Plus的单表操作和分表查询,是代码改造实际落地过程中的一些思考和设计,后续还会继续分享关于Mybatis-Plus使用的一些心得,有兴趣的可以留下你的关注,互相学习。