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

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

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

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:

INSERTINTOatd681_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)            )publicclassSQLInterceptorimplementsInterceptor{// 拦截方法后执行的逻辑@OverridepublicObjectintercept(Invocation invocation)throwsThrowable{// 继续执行Mybatis原有的逻辑// proceed中通过反射执行被拦截的方法returninvocation.proceed();    }// 返回当前拦截的对象(StatementHandler)的动态代理// 当拦截对象的方法被执行时, 动态代理中执行拦截器intercept方法.@OverridepublicObjectplugin(Object target){returnPlugin.wrap(target,this);    }// 设置属性@OverridepublicvoidsetProperties(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)          )publicclassSQLInterceptorimplementsInterceptor{@OverridepublicObject intercept(Invocation invocation)throwsThrowable {// invocation.getArgs()可以获取到被拦截方法的参数// StatementHandler.update(Statement s)的参数为StatementStatement s = (Statement) invocation.getArgs()[0];// 数据源为DRUID, Statement为DRUID的StatementStatement stmt = ((DruidPooledPreparedStatement) s).getStatement();// 配置druid连接时使用filters: stat配置if(stmtinstanceofPreparedStatementProxyImpl) {            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方法returninvocation.proceed();    }}复制代码

只有MySQL提供的PreparedStatement对象中可以获取到最终的SQL.

保存SQL操作需要和Mybatis的操作在同一事务中, 必须同时成功或失败.

5. 测试

在数据库中创建两张表:

atd681_mybatis_testatd681_mybatis_sql

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

// 数据DAO@RepositorypublicinterfaceDataDAO{// 添加数据voidinsert(String dv);// 更新数据voidupdate(String dv);// 删除数据voiddelete();}复制代码

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

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

@RestControllerpublicclassDataController{// 注入DAO@AutowiredprivateDataDAO dao;// 分别执行删除,插入,更新操作// 参数i: 插入时的字符串// 参数u: 更新时的字符串@GetMapping("/mybatis/test")@TransactionalpublicString 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也被保存至数据库中.

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

执行DELETE操作

保存1中DELETE操作的SQL

执行INSERT SQL

保存3中INSERT操作的SQL

执行UPDATE SQL

保存5中UPDATE操作的SQL

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,423评论 0 4
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,894评论 2 89
  • 流年如水,袅袅炊烟暮。 鸡犬声声春又瘦,枝藏几多春梦。 晨醒朝露嘟嘟,夜眠莺语啼啼。 心乐无忧园亩,淡泊最是芳华。
    抱一阅读 328评论 2 2
  • 孩子们早早起来,各自收拾着这次为期一周的行李,这次出发我不给你们整理东西,其因是我不跟着,我给你们整理好了,你们自...
    平安是福王爱云阅读 144评论 0 0
  • 周四晚上一个人背着行李,与灵魂私奔坐着半夜火车到了沈阳,在沈阳的街头走一走全然不顾垂暮已久的天空,深夜一点街...
    溺渡623阅读 329评论 0 0