Mybatis之拦截器——获取执行SQL实现多客户端数据同步

最近的一个项目是将J2EE环境打包安装在客户端(使用 nwjs + NSIS 制作安装包)运行, 所有的业务操作在客户端完成, 数据存储在客户端数据库中. 服务器端数据库汇总各客户端的数据进行分析. 其中客户端ORM使用Mybatis. 通过Mybatis拦截器获取所有在执行的SQL语句, 定期同步至服务器.

本文通过在客户端拦截SQL的操作介绍Mybatis拦截器的使用方法.

1. 项目需求

客户分店较多且比较分散, 部分店内网络不稳定, 客户要求每个分店在无网络的情况下也能正常使用系统, 同时所有店面数据需要进行汇总分析. 综合客户的需求, 项目架构如下:

Mybatis之拦截器——获取执行SQL实现多客户端数据同步_第1张图片

 

将WEB项目及其运行环境通过NSIS制作安装包在各分店进行安装, 每个分店是一个独立的WEB服务, 这样就保证店内在无网络(有局域网,无法访问互联网)的情况下也可以正常使用系统. 此时每个分店的数据库保存自己店内的运营数据, 各店之间的数据相互隔离.

但运营方无法分析所有店面的汇总数据(如商品整体销售情况等), 因此需要将每个店面的数据定期同步至服务器的数据库中.

  • 由于店内可能无网络(无网时不能受数据同步影响,系统需正常运行), 实时同步方案被排除.
  • 为保证数据库安全性, 服务器数据库不能对外暴露, 使用数据库的同步机制方案被排除.
  • 部分业务需要记录数据变化日志(数据从1到0又到1, 需记录过程), 增量同步方案被排除.

最终采用了将客户端所有更新(增,删,改)的SQL按照执行顺序保存至数据库中, 定期同步并在服务器的数据库按照顺序执行SQL, 以此来保证服务器数据库的数据是各客户端数据的汇总.

2. 解决方案

项目采用Mybatis, Mapper 中定义SQL时可以使用Mybatis的标签及参数标识符, Mybatis会解析标签替换参数生成最终的SQL在数据库中执行, 而我们需要的是最终在数据库中执行的SQL.

Mybatis中SQL的写法:


 INSERT INTO atd681_mybatis_test ( dv ) VALUES ( #{dv} )

复制代码

需要同步至服务器执行的SQL:

INSERT INTO atd681_mybatis_test ( dv ) VALUES ( 'aaa' )
复制代码

3. 拦截器

3.1 什么是拦截器

想这样一个场景, 你做饭的时候可能需要以下步骤:

买菜>> 洗菜 >> 切菜 >> 做菜 >> 上菜 >> 洗碗

  • 开始洗菜前, 买菜操作已经完成, 可以知道买了什么菜.
  • 洗菜时还未开始做菜, 因此不知道菜是什么口味的.
  • 在上菜前(此时做菜已经完成), 可以知道菜的口味.
  • 在上菜时不知道有没有剩菜
  • 在洗碗时我们可以知道有没有剩菜.

上面的做饭流程是按照步骤一步一步的进行, 我们既可以在其中的某个步骤中获取前几步的成果, 也可以在某个步骤开始之前做些额外的事情, 比如: 切菜前对菜称重等.

Mybatis提供了这样一个组件: 他可以在某个步骤执行之前先执行自定义的操作. 这个组件叫做拦截器 . 所谓拦截器, 顾名思义: 需要定义拦截哪个操作步骤及拦截后做什么事情.

3.2 定义拦截器

拦截器需要实现 org.apache.ibatis.plugin.Interceptor 接口并指定拦截的方法.

// 拦截器
@Intercepts(@Signature(type = StatementHandler.class, 
 method = "update", 
 args = Statement.class)
 )
public class SQLInterceptor implements Interceptor {
 // 拦截方法后执行的逻辑
 @Override
 public Object intercept(Invocation invocation) throws Throwable {
 // 继续执行Mybatis原有的逻辑
 // proceed中通过反射执行被拦截的方法
 return invocation.proceed();
 }
 // 返回当前拦截的对象(StatementHandler)的动态代理
 // 当拦截对象的方法被执行时, 动态代理中执行拦截器intercept方法.
 @Override
 public Object plugin(Object target) {
 return Plugin.wrap(target, this);
 }
 // 设置属性
 @Override
 public void setProperties(Properties properties) {
 }
}
复制代码
  • @Intercepts 为Mybatis提供的拦截器注解, @Signature 指定拦截的方法.
  • 如果一个拦截器拦截多个方法时, 在 @Intercepts 中配置多个 @Signature (数组)即可.
  • 由于JAVA的方法可以重载, 确定唯一方法需要指定类(type), 方法(method), 参数(args).
  • 拦截器可拦截 Executor , ParameterHandler , ResultSetHandler , StatementHandler 下的方法.

3.3 配置拦截器

在Spring配置文件中, 声明拦截器并将其配置到 SqlSessionFactoryBean 中 plugins 属性中

// Mybatis拦截器
sqlInterceptor(SQLInterceptor)
// Mybatis配置
sqlSessionFactory(SqlSessionFactoryBean) {
 dataSource = ref("dataSource")
 mapperLocations = "classpath*:/com/atd681/mybatis/interceptor/*_mapper.xml"
 
 // 配置Mybatis拦截器
 plugins = [
 sqlInterceptor
 ] 
}
复制代码

4. 获取并保存SQL

Mybatis处理SQL的大致流程如下:

加载SQL>> 解析SQL >> 替换SQL参数 >> 执行SQL >> 获取返回结果

拦截[ 执行SQL ]操作, 此时Mybatis已经完成SQL解析及替换参数, 所得的SQL即为发送数据库执行的SQL. 我们只需要获取该SQL并保存至数据库即可.

// Mybatis拦截器:拦截所有的增删改SQL,将SQL保持至数据库
// 拦截StatementHandler.update方法
@Intercepts(@Signature(type = StatementHandler.class, 
 method = "update", 
 args = Statement.class)
 )
public class SQLInterceptor implements Interceptor {
 @Override
 public Object intercept(Invocation invocation) throws Throwable {
 // invocation.getArgs()可以获取到被拦截方法的参数
 // StatementHandler.update(Statement s)的参数为Statement
 Statement s = (Statement) invocation.getArgs()[0];
 // 数据源为DRUID, Statement为DRUID的Statement
 Statement stmt = ((DruidPooledPreparedStatement) s).getStatement();
 // 配置druid连接时使用filters: stat配置
 if (stmt instanceof PreparedStatementProxyImpl) {
 stmt = ((PreparedStatementProxyImpl) stmt).getRawObject();
 }
 // 数据库提供的Statement可获取参数替换后的SQL(JDBC和DRUID获取的是带?的)
 // 数据库为MySQL,可以直接强制转换为MySQL的PreparedStatement获取SQL
 // SQL在书写时为了格式容器阅读会有换行符(多个空格)存在
 // 为了保存和查看方便去除SQL中的换行及多个空格
 String sql = ((com.mysql.jdbc.PreparedStatement) stmt).asSql().replaceAll("\s+", " ");
 // 保存SQL的操作必须和当前执行的SQL在同一事务中
 // 使用当前SQL所在的数据库连接执行保存操作即可
 // 目标sql成功时保存sql的方法也同步成功
 Connection conn = stmt.getConnection();
 // 将SQL保存至数据库中
 PreparedStatement ps = null;
 try {
 ps = conn.prepareStatement("INSERT INTO atd681_mybatis_sql (v_sql) VALUES (?)");
 ps.setString(1, sql);
 // 因为和Mybatis的操作在同一事务中
 // 如果本次操作如果失败, 所有操作都回滚
 ps.execute();
 }
 finally {
 if (ps != null) {
 ps.close();
 }
 }
 // 继续执行StatementHandler.update方法
 return invocation.proceed();
 }
}
复制代码
  • 只有MySQL提供的PreparedStatement对象中可以获取到最终的SQL.
  • 保存SQL操作需要和Mybatis的操作在同一事务中, 必须同时成功或失败.

5. 测试

在数据库中创建两张表:

atd681_mybatis_test
atd681_mybatis_sql

创建 DAO 和 Mapper , 创建增加, 删除, 修改的方法及SQL

// 数据DAO
@Repository
public interface DataDAO {
 // 添加数据
 void insert(String dv);
 // 更新数据
 void update(String dv);
 // 删除数据
 void delete();
}
复制代码

	
	
	
		INSERT INTO atd681_mybatis_test ( dv ) VALUES ( #{dv} )
	
	
	
	
		UPDATE atd681_mybatis_test1 SET dv = #{dv}
	
	
	
	
		DELETE FROM atd681_mybatis_test
	
	
 
复制代码

控制器中添加方法, 依次调用删除, 添加, 更新. 保证三个操作在同一个事务中.

@RestController
public class DataController {
 // 注入DAO
 @Autowired
 private DataDAO dao;
 // 分别执行删除,插入,更新操作
 // 参数i: 插入时的字符串
 // 参数u: 更新时的字符串
 @GetMapping("/mybatis/test")
 @Transactional
 public String excuteSql(String i, String u) {
 // 删除数据后将参数i的内容插件数据库,将数据更新成参数u的内容
 // 该方法添加了事务,3次数据库操作会在同一个事务中执行.
 // Mybatis拦截器会捕获三次数据库SQL插入至数据库中(详见拦截器)
 dao.delete();
 dao.insert(i);
 dao.update(u);
 return "success";
 }
}
复制代码

启动服务, 访问 http://localhost:3456/mybatis/test?i=insert&u=update

程序依次执行删除、添加(内容为 "insert" )、更新(内容为 "update" )三个操作, 执行完成后数据库中有一条记录(内容为 "update" ). 由于配置了拦截器, 在每个操作执行前将SQL保持至数据库中, 因此三条SQL也被保存至数据库中.

Mybatis之拦截器——获取执行SQL实现多客户端数据同步

 

上述过程中除了3次业务操作, 还有3次保持SQL的操作, 因此数据库总共会执行6条SQL.

  1. 执行DELETE操作
  2. 保存1中DELETE操作的SQL
  3. 执行INSERT SQL
  4. 保存3中INSERT操作的SQL
  5. 执行UPDATE SQL
  6. 保存5中UPDATE操作的SQL

上述6次数据库操作必须在同一事务中, 否则一旦出现业务操作成功但保存SQL失败的情况. 服务器端同步的数据就会与客户端本地不一致.

你可能感兴趣的:(Mybatis之拦截器——获取执行SQL实现多客户端数据同步)