关于使用Mybatis-plus的TableNameHandler动态表名处理器实现分表业务的详细介绍
引言
随着互联网应用的快速发展,数据量呈爆炸式增长。传统的单表设计在面对海量数据时显得力不从心,容易出现性能瓶颈、查询效率低下等问题。为了提高数据库的扩展性和响应速度,分表(Sharding)成为了一种常见的解决方案。
在最近的工作中,遇到了日志过大导致查询过慢的问题(你可能疑问为啥日志要放到mysql中,是因为这个日志只是跨系统之间接口的请求日志)本文主要介绍Mybatis-plus动态表名处理器分表业务的实现。
1. 什么是分表,为什么要分表?
分表是指将一个大型的数据库表拆分成多个较小的表的技术,每个小表存储部分原始表的数据。通过这种方式,可以有效地减少单个表的数据量,从而提升查询效率和系统整体性能。
为什么要分表啊?Mysql是当前互联网系统中使用非常广泛的关系数据库,具有ACID的特性。但是mysql的单表性能会受到表中数据量的限制,主要原因是B+树索引过大导致查询时索引无法全部加载到内存。读取磁盘的次数变多,而磁盘的每次读取对性能都有很大的影响。
这时一个简单可行的方案就是分表(当然土豪也可以堆硬件),将一张数据量庞大的表的数据,拆分到多个表中,这同时也减少了B+树索引的大小,减少磁盘读取次数,提高性能。
2. 分表策略
2.1 水平分表(Horizontal Sharding)
(1)水平分表是根据一定的规则将数据行分散到不同的物理表中。
(2)这种方式保持了表结构的一致性,但数据被分布到了不同的物理位置。
(3)适用于数据量大、写操作频繁的场景。
2.2 垂直分表(Vertical Sharding)
(1)垂直分表是按照列来分割表,将不同字段分配到不同的表中。
(2)主要目的是分离热点数据和非热点数据,或减少单表的宽度。
(3)适用于某些字段访问频率远高于其他字段的情况。
3. 常见的分表技术
3.1 MyCat (原名:MySQL Proxy)
MyCat 是一个开源的分布式数据库系统,它兼容 MySQL 协议,支持 SQL 解析、SQL 路由等功能,能够帮助开发者轻松实现读写分离、分库分表等复杂功能。对于使用 MySQL 数据库的 Java 应用来说,MyCat 提供了一套完整的分表解决方案。
3.2 Apache ShardingSphere
Apache ShardingSphere 是一套开源的分布式数据库中间件,提供了透明化分片、分布式事务、数据加密等功能。它不仅支持 MySQL 和 PostgreSQL 等多种数据库,还为应用程序屏蔽了底层复杂的分片逻辑,使得开发人员可以专注于业务逻辑的实现。
3.3 TDDL (Taobao Distributed Database Layer)
TDDL 是阿里巴巴内部使用的分布式数据库服务层,主要用于解决高并发下的数据库连接池问题和分库分表。它提供了一个简单的 API 接口,允许应用程序以一种非常自然的方式进行分片配置和管理。
3.4 分布式对象关系映射工具(如 Hibernate Shards)
一些 ORM 框架也提供了对分表的支持,例如 Hibernate 的 Shards 扩展。这些工具可以在一定程度上简化分表逻辑的编码工作,使开发者能够更加关注于业务模型的设计而非底层的数据管理细节。
4、使用Mybatis-plus的TableNameHandler动态表名处理器实现分表业务
4.1 优劣
(1)优势
简单易用:对于已采用 MyBatis 或 MyBatis-Plus 的项目来说,使用
TableNameHandler
实现分表逻辑非常直接,不需要引入额外的中间件或大幅修改现有代码架构,降低了迁移成本和技术复杂度。轻量级集成:由于是基于 MyBatis-Plus 进行扩展,因此在集成方面更为轻便,不需要复杂的配置过程,能快速应用于开发环境。
灵活性高:通过自定义
TableNameHandler
,可以根据具体的业务需求灵活地设置分表策略,比如按时间、用户ID或其他业务维度进行分表。
(2)劣势
功能局限性:与专门设计用于分布式数据库管理的解决方案(如 Apache ShardingSphere 或 MyCat)相比,MyBatis-Plus 的
TableNameHandler
在处理复杂分库分表场景时显得力不从心。例如,在需要跨多个数据库实例进行数据操作时,TableNameHandler
可能无法提供足够的支持。缺乏高级特性:MyBatis-Plus 主要关注于简化数据库访问层的操作,对于一些高级特性如分布式事务、全局唯一ID生成等支持有限,这些往往是大规模分布式系统中不可或缺的部分。
性能优化空间有限:虽然 MyBatis-Plus 提供了多种性能优化措施,但在面对超大规模数据集或极高并发请求时,可能还需要依赖更专业的数据库中间件来进行深层次的性能调优。
维护成本:当分表规则变得越来越复杂时,使用
TableNameHandler
可能会导致维护难度增加,尤其是在需要频繁调整分表策略的情况下。
总结而言,如果项目的需求相对简单,主要集中在单个数据库实例内,并且团队对 MyBatis-Plus 已经有一定的熟悉度,那么利用 TableNameHandler
实现分表是一个高效的选择。然而,对于那些需要跨数据库实例、具备复杂查询要求或需要更多高级数据库管理特性的应用场景,选择像 Apache ShardingSphere 或 MyCat 这样的专业工具可能是更好的解决方案。
4.2 详细实现介绍
描述:
(1)这里以跨系统交互日志记录表为例,实现分表业务
(2)当日志表数据越来越大,查询效率越来越低,我们除了添加索引之外,也可以分表操作,提升查询性能
ps:其实也可以将之前的分页查询,改为列表查询,只会进行上一页下一页操作。因为分页查询会计算总数,当数据量过大时也会导致查询过慢
4.2.1 表信息
CREATE TABLE `ihm_system_interaction_log` (`id` bigint(20) NOT NULL COMMENT '主键',`path` varchar(255) NOT NULL COMMENT '路径',`request_part` tinyint(4) DEFAULT NULL COMMENT '请求方:1-患者端 2-医生端 3-his 4-京通(微信)支付 5-首信(医保)支付',`response_part` tinyint(4) DEFAULT NULL COMMENT '响应方:1-患者端 2-医生端 3-his 4-京通(微信)支付 5-首信(医保)支付 6-114公众号支付 7-支付宝支付',`recording_part` tinyint(4) DEFAULT NULL COMMENT '记录方:1-患者端 2-医生端\n同一次请求,请求方和响应方都为患者端或医生端时,需要分别记录,如果请求方和响应方为医生端和第三方his、首信等时,仅记录医生端日志',`request_time` datetime DEFAULT NULL COMMENT '请求时间',`response_time` datetime DEFAULT NULL COMMENT '响应时间',`day` varchar(50) DEFAULT NULL COMMENT '日期:根据请求时间生成,方便检查、统计',`month` int(11) DEFAULT NULL COMMENT '月份',`loss_time` int(11) DEFAULT NULL COMMENT '耗时',`result_code` int(11) DEFAULT NULL COMMENT '响应结果 0-成功 -1 失败',`err_info` longtext COMMENT '错误堆栈:限制长度,超长的截取',`request_param` varchar(64) DEFAULT NULL COMMENT '请求参数',`request_body` longtext COMMENT '请求体:可能是json、可能是xml',`response_body` longtext COMMENT '响应体:可能是json、可能是xml',`message_id` varchar(50) DEFAULT NULL COMMENT '消息id:一个流程的接口日志,应该是同一个message_id',`order_seq` varchar(50) DEFAULT NULL COMMENT '订单编号:涉及订单接口、应该要有对应信息',`user_id_type` tinyint(4) DEFAULT NULL COMMENT '证件类型',`user_id_no` varchar(50) DEFAULT NULL COMMENT '用户证件号码',`user_id` bigint(20) DEFAULT NULL COMMENT '用户id',`patient_id_type` tinyint(4) DEFAULT NULL COMMENT '证件类型',`patient_id_no` varchar(50) DEFAULT NULL COMMENT '用户证件号码',`patient_id` bigint(20) DEFAULT NULL COMMENT '用户id',`trace_id` varchar(255) DEFAULT NULL COMMENT '日志traceId,接口流程的trace_id根据不同方,存不同的trace_id,方便在kibana查看',`hospital_id` bigint(20) DEFAULT NULL COMMENT '医院id',`org_code` varchar(50) DEFAULT NULL COMMENT '机构编码',`deleted` int(11) DEFAULT NULL,`create_time` datetime DEFAULT NULL COMMENT '创建时间',`create_by` varchar(20) DEFAULT NULL COMMENT '创建人',`update_time` datetime DEFAULT NULL COMMENT '更新时间',`update_by` varchar(20) DEFAULT NULL COMMENT '更新人',`version` int(11) DEFAULT NULL COMMENT '版本',PRIMARY KEY (`id`),KEY `order_seq` (`order_seq`),KEY `patient_id_no` (`patient_id_no`),KEY `user_id_no` (`user_id_no`),KEY `ihm_system_interaction_log_create_time_index` (`create_time`),KEY `ihm_system_interaction_log_hospital_id_index` (`hospital_id`),KEY `org_code` (`org_code`),KEY `trace_id` (`trace_id`),KEY `message_id` (`message_id`),KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='跨系统交互日志记录表';
4.2.2 表名按月处理器
月份动态表名处理器
package com.chinaunicom.medical.ihm.handler;import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import lombok.extern.slf4j.Slf4j;import java.util.Arrays;
import java.util.List;@Slf4j
public class MonthTableNameHandler implements TableNameHandler {//用于记录哪些表可以使用该月份动态表名处理器(即哪些表按月分表)private List<String> tableNames;//构造函数,构造动态表名处理器的时候,传递tableNames参数public MonthTableNameHandler(String ...tableNames) {this.tableNames = Arrays.asList(tableNames);}//每个请求线程维护一个month数据,避免多线程数据冲突。所以使用ThreadLocalprivate static final ThreadLocal<String> MONTH_DATA = new ThreadLocal<>();//设置请求线程的month数据public static void setData(String month) {MONTH_DATA.set(month);}//删除当前请求线程的month数据public static void removeData() {MONTH_DATA.remove();}//动态表名接口实现方法@Overridepublic String dynamicTableName(String sql, String tableName) {if (this.tableNames.contains(tableName)){//表名增加月份后缀return tableName + "_" + MONTH_DATA.get();}else{//表名原样返回return tableName;}}}
4.2.3 MybatisPlusConfig配置
这里只添加动态表名处理器 (DynamicTableNameInnerInterceptor
)即可。
动态表名处理器 (
DynamicTableNameInnerInterceptor
):通过实现动态表名处理逻辑,使得某些表在执行 SQL 操作时能够根据特定规则(如时间)选择不同的物理表。乐观锁插件 (
OptimisticLockerInnerInterceptor
):提供乐观锁机制,防止并发修改数据时出现的数据覆盖问题。分页插件 (
PaginationInnerInterceptor
):支持数据库查询结果的分页操作,便于处理大量数据。
package com.chinaunicom.medical.ihm.config;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.chinaunicom.medical.ihm.core.model.DateMetaObjectHandler;
import com.chinaunicom.medical.ihm.handler.MonthTableNameHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;@Configuration
public class PortalMybatisPlusConfig {@Bean@Primarypublic MybatisPlusInterceptor mybatisPlusInterceptor(){MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();dynamicTableNameInnerInterceptor.setTableNameHandler(//可以传多个表名参数,指定哪些表使用MonthTableNameHandler处理表名称new MonthTableNameHandler("ihm_system_interaction_log"));//以拦截器的方式处理表名称interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}}
4.2.4 加载Mybatis-Plus配置
这里只需要添加@AutoConfigureBefore(value = PortalMybatisPlusConfig.class)即可。如果你还有别的通用mybatis-plus配置,可以使用@ComponentScan中的excludeFilters排除。避免配置冲突。
主要注解及功能
@SpringBootApplication:
- 这是一个组合注解,包含了
@Configuration
,@EnableAutoConfiguration
和@ComponentScan
。它简化了 Spring Boot 应用程序的基本设置和自动配置。@EnableScheduling:
- 开启对计划任务的支持,允许使用
@Scheduled
注解来定义定时执行的任务。@ComponentScan:
- 指定了组件扫描的基础包为
com.chinaunicom.medical
,同时通过excludeFilters
排除了MybatisPlusConfig
类的扫描。这意味着在组件扫描过程中不会加载或处理MybatisPlusConfig
这个类。@AutoConfigureBefore(value = PortalMybatisPlusConfig.class):
- 表明当前的自动配置类将在
PortalMybatisPlusConfig
之前被加载和配置。这种配置顺序控制对于确保某些特定的 Bean 在其他 Bean 之前初始化是必要的。核心类和方法
PortalApplication 类:
- 包含了应用程序的入口方法
main
,通过调用SpringApplication.run(PortalApplication.class, args)
来启动 Spring Boot 应用程序。PortalMybatisPlusConfig:
- 提供了 MyBatis-Plus 相关的配置,如动态表名处理器、乐观锁插件和分页插件等。
@ComponentScan(value = {"com.chinaunicom.medical"}, excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {com.chinaunicom.medical.ihm.core.web.MybatisPlusConfig.class})})
@SpringBootApplication
@EnableScheduling
@AutoConfigureBefore(value = PortalMybatisPlusConfig.class)
public class PortalApplication {public static void main(String[] args) {SpringApplication.run(PortalApplication.class, args);}}
4.2.5 定时按月分表
定时创建下月表,已存在不会创建
启动时创建当月与下月表
package com.chinaunicom.medical.ihm.securityaudit.repository.schedule;import com.chinaunicom.medical.ihm.securityaudit.repository.SystemInteractionLogService;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;@Service
public class IhmSystemInteractionLogSchedule {private static final Logger logger = LoggerFactory.getLogger(IhmSystemInteractionLogSchedule.class);@Resourceprivate SystemInteractionLogService systemInteractionLogService;@PostConstructpublic void executeOnStartup() {// 启动时执行的任务逻辑,启动后执行一次,创建当月与下月表createIhmSystemInteractionLogNowTable();createIhmSystemInteractionLogTable();}/*** 启动后,创建一次当月表,已存在不会创建*/public void createIhmSystemInteractionLogNowTable() {String tableName = "`ihm_system_interaction_log_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMM")) + "`";logger.info("----开始创建当月表:{}", tableName);systemInteractionLogService.createIhmSystemInteractionLogTable(tableName);logger.info("----创建当月表结束:{}", tableName);}/*** 每天尝试创建下个月跨系统交互日志表,已存在的不会创建*/@Scheduled(cron = "0 0 4 * * ?")public void createIhmSystemInteractionLogTable() {String tableName = "`ihm_system_interaction_log_" + LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ofPattern("yyyyMM")) + "`";logger.info("----开始创建下月表:{}", tableName);systemInteractionLogService.createIhmSystemInteractionLogTable(tableName);logger.info("----创建下月表结束:{}", tableName);}
}
/*** @author Administrator* @description 针对表【ihm_system_interaction_log(跨系统交互日志记录表)】的数据库操作Service实现* @createDate 2024-07-12 20:20:18*/
@Service
@Slf4j
public class SystemInteractionLogService extends ServiceImpl<SystemInteractionLogMapper, SystemInteractionLog>{@Resourceprivate SystemInteractionLogMapper systemInteractionLogMapper;/*** 创建跨系统交互日志表* @param* @return*/public void createIhmSystemInteractionLogTable(String tableName) {systemInteractionLogMapper.createSystemInteractionLogTable(tableName);}}
package com.chinaunicom.medical.ihm.securityaudit.repository.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.chinaunicom.medical.ihm.securityaudit.repository.model.SystemInteractionLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;/*** @author Administrator* @description 针对表【ihm_system_interaction_log(跨系统交互日志记录表)】的数据库操作Mapper* @createDate 2024-07-12 20:20:18* @Entity com.chinaunicom.medical.ihm.models.po.SystemInteractionLog*/
@Mapper
public interface SystemInteractionLogMapper extends BaseMapper<SystemInteractionLog> {/**** @param tableName*/@Update("CREATE TABLE if not EXISTS ${tableName} \n" +" (`id` bigint(20) NOT NULL COMMENT '主键',\n" +" `path` varchar(255) NOT NULL COMMENT '路径',\n" +" `request_part` tinyint(4) DEFAULT NULL COMMENT '请求方:1-患者端 2-医生端 3-his 4-京通(微信)支付 5-首信(医保)支付',\n" +" `response_part` tinyint(4) DEFAULT NULL COMMENT '响应方:1-患者端 2-医生端 3-his 4-京通(微信)支付 5-首信(医保)支付 6-114公众号支付 7-支付宝支付',\n" +" `recording_part` tinyint(4) DEFAULT NULL COMMENT '记录方:1-患者端 2-医生端\\n同一次请求,请求方和响应方都为患者端或医生端时,需要分别记录,如果请求方和响应方为医生端和第三方his、首信等时,仅记录医生端日志',\n" +" `request_time` datetime DEFAULT NULL COMMENT '请求时间',\n" +" `response_time` datetime DEFAULT NULL COMMENT '响应时间',\n" +" `day` varchar(50) DEFAULT NULL COMMENT '日期:根据请求时间生成,方便检查、统计',\n" +" `month` int(11) DEFAULT NULL COMMENT '月份',\n" +" `loss_time` int(11) DEFAULT NULL COMMENT '耗时',\n" +" `result_code` int(11) DEFAULT NULL COMMENT '响应结果 0-成功 -1 失败',\n" +" `err_info` longtext COMMENT '错误堆栈:限制长度,超长的截取',\n" +" `request_param` varchar(64) DEFAULT NULL COMMENT '请求参数',\n" +" `request_body` longtext COMMENT '请求体:可能是json、可能是xml',\n" +" `response_body` longtext COMMENT '响应体:可能是json、可能是xml',\n" +" `message_id` varchar(50) DEFAULT NULL COMMENT '消息id:一个流程的接口日志,应该是同一个message_id',\n" +" `order_seq` varchar(50) DEFAULT NULL COMMENT '订单编号:涉及订单接口、应该要有对应信息',\n" +" `user_id_type` tinyint(4) DEFAULT NULL COMMENT '证件类型',\n" +" `user_id_no` varchar(50) DEFAULT NULL COMMENT '用户证件号码',\n" +" `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',\n" +" `patient_id_type` tinyint(4) DEFAULT NULL COMMENT '证件类型',\n" +" `patient_id_no` varchar(50) DEFAULT NULL COMMENT '用户证件号码',\n" +" `patient_id` bigint(20) DEFAULT NULL COMMENT '用户id',\n" +" `trace_id` varchar(255) DEFAULT NULL COMMENT '日志traceId,接口流程的trace_id根据不同方,存不同的trace_id,方便在kibana查看',\n" +" `hospital_id` bigint(20) DEFAULT NULL COMMENT '医院id',\n" +" `org_code` varchar(50) DEFAULT NULL COMMENT '机构编码',\n" +" `deleted` int(11) DEFAULT NULL,\n" +" `create_time` datetime DEFAULT NULL COMMENT '创建时间',\n" +" `create_by` varchar(20) DEFAULT NULL COMMENT '创建人',\n" +" `update_time` datetime DEFAULT NULL COMMENT '更新时间',\n" +" `update_by` varchar(20) DEFAULT NULL COMMENT '更新人',\n" +" `version` int(11) DEFAULT NULL COMMENT '版本',\n" +" PRIMARY KEY (`id`),\n" +" KEY `order_seq` (`order_seq`),\n" +" KEY `patient_id_no` (`patient_id_no`),\n" +" KEY `user_id_no` (`user_id_no`),\n" +" KEY `ihm_system_interaction_log_create_time_index` (`create_time`),\n" +" KEY `ihm_system_interaction_log_hospital_id_index` (`hospital_id`),\n" +" KEY `org_code` (`org_code`),\n" +" KEY `trace_id` (`trace_id`),\n" +" KEY `message_id` (`message_id`),\n" +" KEY `user_id` (`user_id`)\n" +") COMMENT='跨系统交互日志记录表';")void createSystemInteractionLogTable(String tableName);
}
4.2.6 查询功能举例
怎么能查到指定表中的数据呢?其实核心在于MonthTableNameHandler日期的处理,这里指定日期后,会被Mybatis-Plus拦截器处理,拼上指定的后缀,得到正确表名。
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM"); String data = simpleDateFormat.format(new Date()); if (StrUtil.isNotBlank(dto.getRequestTime())) {try {Date dt = simpleDateFormat.parse(dto.getRequestTime());data = simpleDateFormat.format(dt);} catch (ParseException e) {log.error("时间转换异常",e);} } MonthTableNameHandler.setData(data.replace("-", ""));
@Overridepublic Page<SystemInteractionLogInfoVO> queryPage(LogQueryPageDTO dto) {Validator.validateNotNull(dto, "传参不能为空");log.info("传参信息:{}", JSONUtil.toJsonPrettyStr(dto));SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM");String data = simpleDateFormat.format(new Date());if (StrUtil.isNotBlank(dto.getRequestTime())) {try {Date dt = simpleDateFormat.parse(dto.getRequestTime());data = simpleDateFormat.format(dt);} catch (ParseException e) {log.error("时间转换异常",e);}}MonthTableNameHandler.setData(data.replace("-", ""));//1、构建分页条件Page<SystemInteractionLog> page = new Page<>(dto.getCurrent(), dto.getSize());//2、分页查询Page<SystemInteractionLog> logPage = lambdaQuery().eq(ObjectUtil.isNotEmpty(dto.getHospitalId()), SystemInteractionLog::getHospitalId, dto.getHospitalId()).eq(ObjectUtil.isNotEmpty(dto.getOrgCode()), SystemInteractionLog::getOrgCode, dto.getOrgCode()).eq(ObjectUtil.isNotEmpty(dto.getOrderSeq()), SystemInteractionLog::getOrderSeq, dto.getOrderSeq()).eq(ObjectUtil.isNotEmpty(dto.getUserId()), SystemInteractionLog::getUserId, dto.getUserId()).eq(StrUtil.isNotBlank(dto.getTraceId()), SystemInteractionLog::getTraceId,dto.getTraceId()).eq(StrUtil.isNotBlank(dto.getMessageId()), SystemInteractionLog::getMessageId,dto.getMessageId()).eq(ObjectUtil.isNotEmpty(dto.getRequestPart()), SystemInteractionLog::getRequestPart, dto.getRequestPart()).eq(ObjectUtil.isNotEmpty(dto.getResponsePart()), SystemInteractionLog::getResponsePart, dto.getResponsePart()).eq(ObjectUtil.isNotEmpty(dto.getRecordingPart()), SystemInteractionLog::getRecordingPart, dto.getRecordingPart()).eq(ObjectUtil.isNotEmpty(dto.getResultCode()), SystemInteractionLog::getResultCode, dto.getResultCode()).like(StrUtil.isNotBlank(dto.getPath()), SystemInteractionLog::getPath, dto.getPath()).ge(ObjectUtil.isNotEmpty(dto.getRequestTime()), SystemInteractionLog::getRequestTime, dto.getRequestTime()).le(ObjectUtil.isNotEmpty(dto.getResponseTime()), SystemInteractionLog::getResponseTime, dto.getResponseTime()).orderByDesc(SystemInteractionLog::getCreateTime).page(page);//3、封装返回结果Page<SystemInteractionLogInfoVO> returnPage = new Page<>();BeanUtil.copyProperties(logPage,returnPage);List<SystemInteractionLog> records = logPage.getRecords();if(CollectionUtil.isNotEmpty(records)){returnPage.setRecords(BeanUtil.copyToList(records,SystemInteractionLogInfoVO.class));}return returnPage;}
4.2.7 保存功能举例
怎么能保存到指定表中的数据呢?其实核心在于MonthTableNameHandler日期的处理,这里指定日期后,会被Mybatis-Plus拦截器处理,拼上指定的后缀,得到正确表名。
//保存数据到,本月对应的表中
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMM");
String data = simpleDateFormat.format(new Date());
MonthTableNameHandler.setData(data);
@Overridepublic boolean saveLog(SystemInteractionLogDto systemInteractionLogDto) {SystemInteractionLog systemInteractionLog = BeanUtil.copyProperties(systemInteractionLogDto, SystemInteractionLog.class);if (StringUtils.isNotBlank(systemInteractionLog.getDay()) && systemInteractionLog.getDay().length() > 10) {systemInteractionLog.setDay(systemInteractionLog.getDay().substring(0, 10));}systemInteractionLog.setMonth(Integer.valueOf(LocalDate.now().format(MONTH_FORMAT)));systemInteractionLog.setOrgCode(systemInteractionLogDto.getHospCode());String orderSeq = systemInteractionLog.getOrderSeq();String inquiryNo = null;String hospCode = null;if (StringUtils.isBlank(orderSeq)) {if (StringUtils.isNotBlank(systemInteractionLog.getRequestParam()) && systemInteractionLog.getRequestParam().contains("orderSeq")) {String requestParam = systemInteractionLog.getRequestParam();String[] params = requestParam.split("=", -1);for (String param : params) {if (StringUtils.isBlank(orderSeq) && ("orderSeq".equals(param) || "order_seq".equals(param))) {orderSeq = param.split("=")[1];}if (StringUtils.isBlank(inquiryNo) && ("inquiryNo".equals(param) || "inquiry_no".equals(param))) {inquiryNo = param.split("=")[1];}if (StringUtils.isBlank(hospCode) && ("hosp_code".equals(param) || "hospCode".equals(param))) {hospCode = param.split("=")[1];}}}}if (StringUtils.isNotBlank(systemInteractionLog.getRequestBody())) {String requestBody = systemInteractionLog.getRequestBody();if (StringUtils.isBlank(orderSeq)) {orderSeq = getTargetValueByKey(requestBody, "orderSeq");}if (StringUtils.isBlank(orderSeq)) {orderSeq = getTargetValueByKey(requestBody, "order_seq");}if (StringUtils.isBlank(inquiryNo)) {inquiryNo = getTargetValueByKey(requestBody, "inquiryNo");}if (StringUtils.isBlank(inquiryNo)) {inquiryNo = getTargetValueByKey(requestBody, "inquiry_no");}if (StringUtils.isBlank(hospCode)) {hospCode = getTargetValueByKey(requestBody, "hospCode");}if (StringUtils.isBlank(hospCode)) {hospCode = getTargetValueByKey(requestBody, "hosp_code");}}if (StringUtils.isNotBlank(systemInteractionLog.getResponseBody())) {String responseBody = systemInteractionLog.getResponseBody();if (StringUtils.isBlank(orderSeq)) {orderSeq = getTargetValueByKey(responseBody, "orderSeq");}if (StringUtils.isBlank(orderSeq)) {orderSeq = getTargetValueByKey(responseBody, "order_seq");}if (StringUtils.isBlank(inquiryNo)) {inquiryNo = getTargetValueByKey(responseBody, "inquiryNo");}if (StringUtils.isBlank(inquiryNo)) {inquiryNo = getTargetValueByKey(responseBody, "inquiry_no");}if (StringUtils.isBlank(hospCode)) {hospCode = getTargetValueByKey(responseBody, "hospCode");}if (StringUtils.isBlank(hospCode)) {hospCode = getTargetValueByKey(responseBody, "hosp_code");}}if (StringUtils.isBlank(systemInteractionLog.getOrgCode())) {systemInteractionLog.setOrgCode(hospCode);}if (StringUtils.isBlank(orderSeq) && (StringUtils.isNotBlank(systemInteractionLog.getOrgCode()) && StringUtils.isNotBlank(inquiryNo))) {Result<OrderDetailVO> result = orderApi.queryOrderDetailByInquiryNo(systemInteractionLog.getOrgCode(), inquiryNo);if (Objects.nonNull(result) && Objects.nonNull(result.getData())) {OrderDetailVO orderDetailVO = result.getData();systemInteractionLog.setPatientId(orderDetailVO.getPatientId());systemInteractionLog.setPatientIdNo(orderDetailVO.getIdCard());systemInteractionLog.setPatientIdType(orderDetailVO.getIdCardType());systemInteractionLog.setUserId(orderDetailVO.getUserId());systemInteractionLog.setHospitalId(orderDetailVO.getHospitalId());systemInteractionLog.setOrgCode(orderDetailVO.getOrgCode());systemInteractionLog.setOrderSeq(orderDetailVO.getOrderSeq());}}try {//补充字段if (StringUtils.isNotBlank(orderSeq)) {systemInteractionLog.setOrderSeq(orderSeq);if (systemInteractionLog.getPath().contains("/reg/occupy")) {Thread.sleep(3000L);}Result<OrderDetailVO> result = orderApi.queryOrderDetailBySeq(orderSeq);if (Objects.nonNull(result) && Objects.nonNull(result.getData())) {OrderDetailVO orderDetailVO = result.getData();systemInteractionLog.setPatientId(orderDetailVO.getPatientId());systemInteractionLog.setPatientIdNo(orderDetailVO.getIdCard());systemInteractionLog.setPatientIdType(orderDetailVO.getIdCardType());systemInteractionLog.setUserId(orderDetailVO.getUserId());systemInteractionLog.setHospitalId(orderDetailVO.getHospitalId());systemInteractionLog.setOrgCode(orderDetailVO.getOrgCode());}}} catch (Exception e) {log.warn("错误参数:{}", e.getMessage());}if (StringUtils.isBlank(systemInteractionLog.getOrgCode()) && StringUtils.isNotBlank(systemInteractionLogDto.getTenantId())) {Result<HospitalVo> result = hospitalApi.queryByTenantId(systemInteractionLogDto.getTenantId());if (Objects.nonNull(result) && Objects.nonNull(result.getData())) {systemInteractionLog.setHospitalId(result.getData().getHospitalId());systemInteractionLog.setOrgCode(result.getData().getOrgCode());}} else if (StringUtils.isNotBlank(systemInteractionLog.getOrgCode()) && systemInteractionLog.getOrgCode().length() >= 8) {Result<HospitalVo> result = hospitalApi.queryByOrgCode(systemInteractionLog.getOrgCode());if (Objects.nonNull(result) && Objects.nonNull(result.getData())) {systemInteractionLog.setHospitalId(result.getData().getHospitalId());systemInteractionLog.setOrgCode(result.getData().getOrgCode());}}//保存数据到,本月对应的表中SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMM");String data = simpleDateFormat.format(new Date());MonthTableNameHandler.setData(data);// 自增idLong id = redisTemplate.opsForValue().increment("ihm_system_interaction_log");systemInteractionLog.setId(id);return save(systemInteractionLog);}
4.3 分表效果
相关文章:

关于使用Mybatis-plus的TableNameHandler动态表名处理器实现分表业务的详细介绍
引言 随着互联网应用的快速发展,数据量呈爆炸式增长。传统的单表设计在面对海量数据时显得力不从心,容易出现性能瓶颈、查询效率低下等问题。为了提高数据库的扩展性和响应速度,分表(Sharding)成为了一种常见的解决方案…...

docker 安装 redis 详解
在平常的开发工作中,我们经常会用到 redis,那么 docker 下应该如何安装 redis 呢?简单来说:第一步:拉取redis镜像;第二步:设置 redis.conf 配置文件;第三步:编写 docker-…...

56. 合并区间
【题目】:56. 合并区间 class Solution { public:vector<vector<int>> merge(vector<vector<int>>& intervals) {// 按照左端点排序sort(intervals.begin(), intervals.end(), [&](vector<int> lhs, vector<int> rhs)…...

BOM对象location与数组操作结合——查询串提取案例
BOM对象location与数组操作结合——查询串提取案例 前置知识 1. Location 对象 Location 对象是 JavaScript 提供的内置对象之一,它表示当前窗口或框架的 URL,并允许你通过它操作或获取 URL 的信息。可以通过 window.location 访问。 主要属性&#…...

Jetson Orin Nano Super之 onnxruntime 编译安装
Jetson Orin Nano Super之 onnxruntime 编译安装 1. 源由2. 步骤步骤一:安装3.26 cmake步骤二:下载代码步骤三:编译代码步骤四:找到安装包步骤五:安装whl包 3. 注意4. 参考资料 1. 源由 Build onnxruntime 1.19.2 fai…...

开发环境搭建-3:配置 nodejs 开发环境 (fnm+ node + pnpm)
在 WSL 环境中配置:WSL2 (2.3.26.0) Oracle Linux 8.7 官方镜像 node 官网:https://nodejs.org/zh-cn/download 点击【下载】,选择想要的 node 版本、操作系统、node 版本管理器、npm包管理器 根据下面代码提示依次执行对应代码即可 基本概…...

[SWPUCTF 2022 新生赛]js_sign
题目 查看页面源代码 <!DOCTYPE html> <html> <head><meta charset"utf-8"><style>body {background-color: rgb(255, 255, 255);}</style> </head> <body><input id"flag" /><button>Check…...

农业信息化的基本框架
农业信息化的主要研究内容 基于作物模型的相关研究 作物生长模拟模型以及模型评价、模型的应用作物模型应用,包括:作物生态系统过程、生产管理措施、区域作物产量评估与气候变化对产量影响预测、基于作物模型的决策支持系统 数据挖掘、知识工程及应用、管…...

OpenAI的真正对手?DeepSeek-R1如何用强化学习重构LLM能力边界——DeepSeek-R1论文精读
2025年1月20日,DeepSeek-R1 发布,并同步开源模型权重。截至目前,DeepSeek 发布的 iOS 应用甚至超越了 ChatGPT 的官方应用,直接登顶 AppStore。 DeepSeek-R1 一经发布,各种资讯已经铺天盖地,那就让我们一起…...

Vue 3 中的父子组件传值:详细示例与解析
在 Vue 3 中,父子组件之间的数据传递是一个常见的需求。父组件可以通过 props 将数据传递给子组件,而子组件可以通过 defineProps 接收这些数据。本文将详细介绍父子组件传值的使用方法,并通过优化后的代码示例演示如何实现。 1. 父子组件传值…...

回顾2024,展望2025
项目 LMD performance phase2 今年修修补补,设计和做了很多item,有时候自己都数不清做了什么大大小小的item,但是for LMD performance phase2的go-live确实是最大也是最难的了,无论什么系统,只要用的人多了ÿ…...

【Python实现机器遗忘算法】复现2021年顶会 AAAI算法Amnesiac Unlearning
【Python实现机器遗忘算法】复现2021年顶会 AAAI算法Amnesiac Unlearning 1 算法原理 论文:Graves, L., Nagisetty, V., & Ganesh, V. (2021). Amnesiac machine learning. In Proceedings of the AAAI Conference on Artificial Intelligence, volume 35, 115…...

Vue 3 30天精进之旅:Day 03 - Vue实例
引言 在前两天的学习中,我们成功搭建了Vue.js的开发环境,并创建了我们的第一个Vue项目。今天,我们将深入了解Vue的核心概念之一——Vue实例。通过学习Vue实例,你将理解Vue的基础架构,掌握数据绑定、模板语法和指令的使…...

【ArcGIS微课1000例】0141:提取多波段影像中的单个波段
文章目录 一、波段提取函数二、加载单波段导出问题描述:如下图所示,img格式的时序NDVI数据有24个波段。现在需要提取某一个波段,该怎样操作? 一、波段提取函数 首先加载多波段数据。点击【窗口】→【影像分析】。 选择需要处理的多波段影像,点击下方的【添加函数】。 在多…...

【第九天】零基础入门刷题Python-算法篇-数据结构与算法的介绍-六种常见的图论算法(持续更新)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、Python数据结构与算法的详细介绍1.Python中的常用的图论算法2. 图论算法3.详细的图论算法1)深度优先搜索(DFS)2…...

落地 轮廓匹配
个人理解为将一幅不规则的图形,通过最轮廓发现,最大轮廓匹配来确定图像的位置,再通过pt将不规则的图像放在规定的矩形里面,在通过透视变换将不规则的图形放进规则的图像中。 1. findHomography 函数 • Mat h findHomography(s…...

【漫话机器学习系列】064.梯度下降小口诀(Gradient Descent rule of thume)
梯度下降小口诀 为了帮助记忆梯度下降的核心原理和关键注意事项,可以用以下简单口诀来总结: 1. 基本原理 损失递减,梯度为引:目标是让损失函数减少,依靠梯度指引方向。负梯度,反向最短:沿着负…...

JAVA(SpringBoot)集成Kafka实现消息发送和接收。
SpringBoot集成Kafka实现消息发送和接收。 一、Kafka 简介二、Kafka 功能三、POM依赖四、配置文件五、生产者六、消费者 君子之学贵一,一则明,明则有功。 一、Kafka 简介 Kafka 是由 Apache 软件基金会开发的一个开源流处理平台,最初由 Link…...

AI刷题-蛋糕工厂产能规划、优质章节的连续选择
挑两个简单的写写 目录 一、蛋糕工厂产能规划 问题描述 输入格式 输出格式 解题思路: 问题理解 数据结构选择 算法步骤 关键点 最终代码: 运行结果:编辑 二、优质章节的连续选择 问题描述 输入格式 输出格式 解题思路&a…...

在线可编辑Excel
1. Handsontable 特点: 提供了类似 Excel 的表格编辑体验,包括单元格样式、公式计算、数据验证等功能。 支持多种插件,如筛选、排序、合并单元格等。 轻量级且易于集成到现有项目中。 具备强大的自定义能力,可以调整外观和行为…...

什么是词嵌入?Word2Vec、GloVe 与 FastText 的区别
自然语言处理(NLP)领域的核心问题之一,是如何将人类的语言转换成计算机可以理解的数值形式,而词嵌入(Word Embedding)正是为了解决这个问题的重要技术。本文将详细讲解词嵌入的概念及其经典模型(Word2Vec、GloVe 和 FastText)的原理与区别。 1. 什么是词嵌入(Word Em…...

WPS数据分析000010
基于数据透视表的内容 一、排序 手动调动 二、筛选 三、值显示方式 四、值汇总依据 五、布局和选项 不显示分类汇总 合并居中带标签的单元格 空单元格显示 六、显示报表筛选页...

Qt中QVariant的使用
1.使用QVariant实现不同类型数据的相加 方法:通过type函数返回数值的类型,然后通过setValue来构造一个QVariant类型的返回值。 函数: QVariant mainPage::dataPlus(QVariant a, QVariant b) {QVariant ret;if ((a.type() QVariant::Int) &a…...

Avalonia UI MVVM DataTemplate里绑定Command
Avalonia 模板里面绑定ViewModel跟WPF写法有些不同。需要单独绑定Command. WPF里面可以直接按照下面的方法绑定DataContext. <Button Content"Button" Command"{Binding DataContext.ClickCommand, RelativeSource{RelativeSource AncestorType{x:Type User…...

动态规划DP 数字三角型模型 最低通行费用(题目详解+C++代码完整实现)
最低通行费用 原题链接 AcWing 1018. 最低同行费用 题目描述 一个商人穿过一个 NN的正方形的网格,去参加一个非常重要的商务活动。 他要从网格的左上角进,右下角出。每穿越中间 1个小方格,都要花费 1个单位时间。商人必须在 (2N−1)个单位…...

deepseek R1的确不错,特别是深度思考模式
deepseek R1的确不错,特别是深度思考模式,每次都能自我反省改进。比如我让 它写文案: 【赛博朋克版程序员新春密码——2025我们来破局】 亲爱的代码骑士们: 当CtrlS的肌肉记忆遇上抢票插件,当Spring Boot的…...

Linux 常用命令 - sort 【对文件内容进行排序】
简介 sort 命令源于英文单词 “sort”,表示排序。其主要功能是对文本文件中的行进行排序。它可以根据字母、数字、特定字段等不同的标准进行排序。sort 通过逐行读取文件(没有指定文件或指定文件为 - 时读取标准输入)内容,并按照…...

MyBatis最佳实践:提升数据库交互效率的秘密武器
第一章:框架的概述: MyBatis 框架的概述: MyBatis 是一个优秀的基于 Java 的持久框架,内部对 JDBC 做了封装,使开发者只需要关注 SQL 语句,而不关注 JDBC 的代码,使开发变得更加的简单MyBatis 通…...

选择困难?直接生成pynput快捷键字符串
from pynput import keyboard# 文档:https://pynput.readthedocs.io/en/latest/keyboard.html#monitoring-the-keyboard # 博客(pynput相关源码):https://blog.csdn.net/qq_39124701/article/details/145230331 # 虚拟键码(十六进制):https:/…...

DeepSeek-R1:强化学习驱动的推理模型
1月20日晚,DeepSeek正式发布了全新的推理模型DeepSeek-R1,引起了人工智能领域的广泛关注。该模型在数学、代码生成等高复杂度任务上表现出色,性能对标OpenAI的o1正式版。同时,DeepSeek宣布将DeepSeek-R1以及相关技术报告全面开源。…...