背景

随着业务的发展和合规要求,产品数据库将切换到Postgres。之前不同技术域,不同交付工程的数据分库管理的方式切换到PG数据库后将通过分schema管理。
ORM继续使用Mybatis,为使用迁移工作量尽可能小,现有的SQL代码不做大的修改。动态数据源实现考虑在Mybatis执行过程中做拦截,替换sql中的schema标识。

提取请求参数中的schema

约定rest接口请求Header参数中增加schema信息。通过切面技术从请求头中提取schema后保存到线程变量。

1. 提取schema

package com.postgres.manager;import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;/*** Schema切面, 提取header头中的schema保存到SchemaHolder中** @author elon* @since 2022-03-20*/
@Aspect
@Component
@Order(9999)
public class SchemaAspect {@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping) "+ "|| @annotation(org.springframework.web.bind.annotation.PostMapping) "+ "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping) "+ "|| @annotation(org.springframework.web.bind.annotation.RequestMapping)")void schema() {}/*** 从请求头提取** @param joinPoint*/@Before("schema()")public void setSchema(JoinPoint joinPoint) {String schema = getSchemaFromHeader();SchemaHolder.set(schema);}@After("schema()")public void clearSchema(JoinPoint joinPoint) {SchemaHolder.clear();}/*** 从请求头中后去schema信息** @return schema*/private String getSchemaFromHeader() {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String schema = request.getHeader("schema");return schema;}
}

2. 保存schema的线程变量类

package com.postgres.manager;/*** Schema持有类. 用于在异步线程或者跨多个方法传递schema信息** @author elon* @since 2022-03-19*/
public class SchemaHolder {private static ThreadLocal<String> schema = new ThreadLocal<>();public static void set(String sch) {schema.set(sch);}public static String get() {return schema.get();}public static void clear() {schema.remove();}
}

定义Mybatis拦截器

1. 定义拦截器注解,用于修饰DAO层级接口

package com.postgres.manager;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** schema拦截器注解。修饰mapper接口类,用以区分访问的pg数据库schema** @author elon* @since 2022-03-20*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SchemaInterceptAnnotation {/*** schema类型。取值范围:business, common** @return*/String schemaType() default "";
}

在DAO层接口类加上该注解,拦截器会动态切换schema.

package com.postgres.mapper;import com.postgres.manager.SchemaInterceptAnnotation;
import com.postgres.model.ExamResult;
import com.postgres.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;@Mapper
@SchemaInterceptAnnotation(schemaType = "business")
public interface UserMapper {/*** 从schema获取user数据** @return user列表*/List<User> getUserFromSchema(@Param("name") String name);/*** 插入用户数据到schema** @param userList 用户列表*/void insertUser2Schema(@Param("list") List<User> userList);/*** 获取测试成绩.** @return 测试成绩列表*/List<ExamResult> getExamResult();
}

2. 拦截器替换sql中的表名为schema.表名

package com.postgres.manager;import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.Properties;/*** StatementHandler拦截器. 在prepare方法执行前拦截,修改sql语句,增加schema.** @author elon* @since 2022-03-20*/
@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class StatementHandlerInterceptor implements Interceptor {private static final Logger LOGGER = LoggerFactory.getLogger(StatementHandlerInterceptor.class);/*** 业务数据分schema存储*/private static final String BUSINESS_SCHEMA = "business";/*** 公共的配置数据(不分schema), 固定库*/private static final String COMMON_SCHEMA = "common";@Overridepublic Object intercept(Invocation invocation) throws Throwable {StatementHandler statementHandler = (StatementHandler) invocation.getTarget();MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");String mapperMethod = mappedStatement.getId();BoundSql boundSql = statementHandler.getBoundSql();String sql = boundSql.getSql();String mapperClass = mapperMethod.substring(0, mappedStatement.getId().lastIndexOf("."));Class<?> classType = Class.forName(mapperClass);SchemaInterceptAnnotation interceptAnnotation = classType.getAnnotation(SchemaInterceptAnnotation.class);String schemaType = interceptAnnotation.schemaType();String newSql = replaceSqlWithSchema(schemaType, sql, mapperMethod);//通过反射修改sql语句Field field = boundSql.getClass().getDeclaredField("sql");field.setAccessible(true);field.set(boundSql, newSql);return invocation.proceed();}@Overridepublic Object plugin(Object object) {if (object instanceof StatementHandler) {return Plugin.wrap(object, this);} else {return object;}}@Overridepublic void setProperties(Properties properties) {}private String replaceSqlWithSchema(String schemaType, String originalSql, String mapperMethod){// 替换sql中的表名,加上schemaif (BUSINESS_SCHEMA.equals(schemaType)) {String schema = SchemaHolder.get();return originalSql.replaceAll(" t_", " " + schema + ".t_");} else if (COMMON_SCHEMA.equals(schemaType)) {return originalSql.replaceAll(" t_", " " + COMMON_SCHEMA + ".t_");} else {LOGGER.error("Invalid SchemaInterceptAnnotation. mapperMethod:{}", mapperMethod);throw new IllegalArgumentException("Invalid SchemaInterceptAnnotation.");}}
}

2. 添加拦截器

加上如下处理, 拦截器才会生效

package com.postgres.config;import com.postgres.manager.StatementHandlerInterceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;import javax.annotation.PostConstruct;
import java.util.List;@Configuration
public class InterceptorConfig {@Autowiredprivate List<SqlSessionFactory> sqlSessionFactoryList;@PostConstructpublic void addSqlInterceptor() {StatementHandlerInterceptor interceptor = new StatementHandlerInterceptor();for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {sqlSessionFactory.getConfiguration().addInterceptor(interceptor);}}
}

完整的Demo代码还包括DataSource配置和XML中SQL,这些和普通的Spring Boot项目无异。参考github上的完整实现代码:https://github.com/ylforever/elon-postgres

定义Mybatis拦截器动态切换postgre数据库schema相关推荐

  1. 老年人教程:MyBatis拦截器动态修改SQL(更新与插入)语句

    注:本文编写与 2019年12月17日, 内容可能存在时效性问题. 数据库使用MySQL5.7 集成于SpringBoot 2.0.X , 引用国产的开源工具类Hutool 本教程建议显示大纲视图 配 ...

  2. list mybatis 接收 类型_基于mybatis拦截器实现的一款简易影子表自动切换插件

    近期因工作需要,小编基于mybatis拦截器开发了一款简易影子表自动切换插件,可以根据配置实现动态修改表名,即将对原source table表的操作自动切换到对target table表的操作.该插件 ...

  3. mybatis手动切换数据库_在Spring项目中使用 Mybatis 如何实现动态切换数据源

    在Spring项目中使用 Mybatis 如何实现动态切换数据源 发布时间:2020-11-17 16:20:11 来源:亿速云 阅读:108 作者:Leah 这篇文章将为大家详细讲解有关在Sprin ...

  4. java怎么拦截数据库查询结果_关于mybatis拦截器,有谁知道怎么对结果集进行拦截,将指定字段查询结果进行格式化...

    用MyBatis结果集拦截器做过这样一个需求: 由于项目需求经常变动,项目MySQL数据库都是存放JSON字符串,例如:用户的基本信息随着版本升级可能会有变动 数据表 CREATE TABLE `ac ...

  5. java使用mybatis拦截器对数据库敏感字段进行加密存储并解密

    记录业务中遇到的使用场景:灵活对数据库敏感字段进行加密和解密 文章目录 前言 一.创建数据库表和实体类 二.Mapper.Service.Controller等 三.自定义注解 四.加密工具类 五.参 ...

  6. 面试官:你能说说MyBatis拦截器原理吗?

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 作者:Format cnblogs.com/fangjian042 ...

  7. insert into select 主键自增_springboot2结合mybatis拦截器实现主键自动生成

    点击上方蓝字关注我们 1 01 前言 前阵子和朋友聊天,他说他们项目有个需求,要实现主键自动生成,不想每次新增的时候,都手动设置主键.于是我就问他,那你们数据库表设置主键自动递增不就得了.他的回答是他 ...

  8. by mybatis 自定义order_springboot2结合mybatis拦截器实现主键自动生成

    点击上方蓝字关注我们 1 01 前言 前阵子和朋友聊天,他说他们项目有个需求,要实现主键自动生成,不想每次新增的时候,都手动设置主键.于是我就问他,那你们数据库表设置主键自动递增不就得了.他的回答是他 ...

  9. 真正理解mybatis拦截器以及Interceptor和Plugin作用

    看了很多博客文章和,mybatis 的拦截器概念还是不能很好理解, 可能是因为自己基础不好或者理解方式和他人不同吧,所以决定自己花时间好好捋捋, 然后把理解后的总结记录下来,供他人参考,也许你们的理解 ...

最新文章

  1. 一文看懂人脸识别技术发展脉络
  2. Vue.js 生产环境部署
  3. 第一夜 主公说啥俺做啥
  4. 《深入理解java虚拟机》第1章 走近Java
  5. apicloud入门学习笔记1:简单介绍
  6. IOCP模型TCP服务器
  7. Git教程_2 所有操作讲解
  8. 跨域——vue中的axios.post使用json数据传输,出现请求头字段内容类型是不被允许的情况的解决方案
  9. txt转excel 处理数据
  10. matlab 同态滤波
  11. 三个百分数相乘计算机,我的公考笔记:资料分析的三个速算技巧
  12. 新计算机的word无法输入文字,Win10专业版系统为什么不能给电脑Word输入中文汉字...
  13. 基于51单片机的烟雾火灾报警系统
  14. 2022危险化学品生产单位主要负责人考试题库及模拟考试
  15. CodeSys轴控指令使用方法
  16. 天才少年!他们的内心世界你懂吗?道翰天琼认知智能机器人平台API接口为您揭秘。
  17. 浅谈人工智能神经网络的优点
  18. 《随机过程》学习笔记--高斯过程(1)
  19. C/C++内存检测工具valgrind
  20. c 语言中eof 是什么,什么是C编程语言中的EOF?

热门文章

  1. Ubuntu18.04|20.04 idea安装Consolas字体
  2. GPS从入门到放弃(十一) --- 差分GPS
  3. Github css加载失败,样式混乱解决办法
  4. 『牛客|每日一题』走迷宫
  5. 认亲app如何创建小家谱
  6. 巧用PDF编辑器裁剪功能去除PDF广告
  7. ubuntu18.04 aria2 GUI 的安装使用
  8. 软件产品测试之压力测试
  9. C++无法打开FDB文件
  10. 时间戳 转换24小时制