Mybatis之?dāng)r截器--獲取執(zhí)行SQL實現(xiàn)多客戶端數(shù)據(jù)同步

最近的一個項目是將J2EE環(huán)境打包安裝在客戶端(使用?nwjs?+?NSIS?制作安裝包)運行, 所有的業(yè)務(wù)操作在客戶端完成, 數(shù)據(jù)存儲在客戶端數(shù)據(jù)庫中. 服務(wù)器端數(shù)據(jù)庫匯總各客戶端的數(shù)據(jù)進行分析. 其中客戶端ORM使用Mybatis. 通過Mybatis攔截器獲取所有在執(zhí)行的SQL語句, 定期同步至服務(wù)器.

本文通過在客戶端攔截SQL的操作介紹Mybatis攔截器的使用方法.

1. 項目需求

客戶分店較多且比較分散, 部分店內(nèi)網(wǎng)絡(luò)不穩(wěn)定, 客戶要求每個分店在無網(wǎng)絡(luò)的情況下也能正常使用系統(tǒng), 同時所有店面數(shù)據(jù)需要進行匯總分析. 綜合客戶的需求, 項目架構(gòu)如下:

將WEB項目及其運行環(huán)境通過NSIS制作安裝包在各分店進行安裝, 每個分店是一個獨立的WEB服務(wù), 這樣就保證店內(nèi)在無網(wǎng)絡(luò)(有局域網(wǎng),無法訪問互聯(lián)網(wǎng))的情況下也可以正常使用系統(tǒng). 此時每個分店的數(shù)據(jù)庫保存自己店內(nèi)的運營數(shù)據(jù), 各店之間的數(shù)據(jù)相互隔離.

但運營方無法分析所有店面的匯總數(shù)據(jù)(如商品整體銷售情況等), 因此需要將每個店面的數(shù)據(jù)定期同步至服務(wù)器的數(shù)據(jù)庫中.

由于店內(nèi)可能無網(wǎng)絡(luò)(無網(wǎng)時不能受數(shù)據(jù)同步影響,系統(tǒng)需正常運行), 實時同步方案被排除.

為保證數(shù)據(jù)庫安全性, 服務(wù)器數(shù)據(jù)庫不能對外暴露, 使用數(shù)據(jù)庫的同步機制方案被排除.

部分業(yè)務(wù)需要記錄數(shù)據(jù)變化日志(數(shù)據(jù)從1到0又到1, 需記錄過程), 增量同步方案被排除.

最終采用了將客戶端所有更新(增,刪,改)的SQL按照執(zhí)行順序保存至數(shù)據(jù)庫中, 定期同步并在服務(wù)器的數(shù)據(jù)庫按照順序執(zhí)行SQL, 以此來保證服務(wù)器數(shù)據(jù)庫的數(shù)據(jù)是各客戶端數(shù)據(jù)的匯總.

2. 解決方案

項目采用Mybatis,?Mapper?中定義SQL時可以使用Mybatis的標(biāo)簽及參數(shù)標(biāo)識符, Mybatis會解析標(biāo)簽替換參數(shù)生成最終的SQL在數(shù)據(jù)庫中執(zhí)行, 而我們需要的是最終在數(shù)據(jù)庫中執(zhí)行的SQL.

Mybatis中SQL的寫法:

INSERT INTO atd681_mybatis_test ( dv ) VALUES ( #{dv})復(fù)制代碼

需要同步至服務(wù)器執(zhí)行的SQL:

INSERTINTOatd681_mybatis_test ( dv )VALUES('aaa')復(fù)制代碼

3. 攔截器

3.1 什么是攔截器

想這樣一個場景, 你做飯的時候可能需要以下步驟:

買菜>>?洗菜?>>?切菜?>>?做菜?>>?上菜?>>?洗碗

開始洗菜前, 買菜操作已經(jīng)完成, 可以知道買了什么菜.

洗菜時還未開始做菜, 因此不知道菜是什么口味的.

在上菜前(此時做菜已經(jīng)完成), 可以知道菜的口味.

在上菜時不知道有沒有剩菜

在洗碗時我們可以知道有沒有剩菜.

上面的做飯流程是按照步驟一步一步的進行, 我們既可以在其中的某個步驟中獲取前幾步的成果, 也可以在某個步驟開始之前做些額外的事情, 比如: 切菜前對菜稱重等.

Mybatis提供了這樣一個組件: 他可以在某個步驟執(zhí)行之前先執(zhí)行自定義的操作. 這個組件叫做?攔截器?. 所謂攔截器, 顧名思義: 需要定義攔截哪個操作步驟及攔截后做什么事情.

3.2 定義攔截器

攔截器需要實現(xiàn)?org.apache.ibatis.plugin.Interceptor?接口并指定攔截的方法.

// 攔截器@Intercepts(@Signature(type = StatementHandler.class,? ? ? ? ? ? ? ? ? ? ? ? method ="update",? ? ? ? ? ? ? ? ? ? ? ? args = Statement.class)? ? ? ? ? ? )publicclassSQLInterceptorimplementsInterceptor{// 攔截方法后執(zhí)行的邏輯@OverridepublicObjectintercept(Invocation invocation)throwsThrowable{// 繼續(xù)執(zhí)行Mybatis原有的邏輯// proceed中通過反射執(zhí)行被攔截的方法returninvocation.proceed();? ? }// 返回當(dāng)前攔截的對象(StatementHandler)的動態(tài)代理// 當(dāng)攔截對象的方法被執(zhí)行時, 動態(tài)代理中執(zhí)行攔截器intercept方法.@OverridepublicObjectplugin(Object target){returnPlugin.wrap(target,this);? ? }// 設(shè)置屬性@OverridepublicvoidsetProperties(Properties properties){? ? }}復(fù)制代碼

@Intercepts?為Mybatis提供的攔截器注解,?@Signature?指定攔截的方法.

如果一個攔截器攔截多個方法時, 在?@Intercepts?中配置多個?@Signature?(數(shù)組)即可.

由于JAVA的方法可以重載, 確定唯一方法需要指定類(type), 方法(method), 參數(shù)(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"http:// 配置Mybatis攔截器plugins = [? ? ? ? sqlInterceptor? ? ] }復(fù)制代碼

4. 獲取并保存SQL

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

加載SQL>>?解析SQL?>>?替換SQL參數(shù)?>>?執(zhí)行SQL?>>?獲取返回結(jié)果

攔截[?執(zhí)行SQL?]操作, 此時Mybatis已經(jīng)完成SQL解析及替換參數(shù), 所得的SQL即為發(fā)送數(shù)據(jù)庫執(zhí)行的SQL. 我們只需要獲取該SQL并保存至數(shù)據(jù)庫即可.

// Mybatis攔截器:攔截所有的增刪改SQL,將SQL保持至數(shù)據(jù)庫// 攔截StatementHandler.update方法@Intercepts(@Signature(type = StatementHandler.class,? ? ? ? ? ? ? ? ? ? ? ? method ="update",? ? ? ? ? ? ? ? ? ? ? ? args = Statement.class)? ? ? ? ? )publicclassSQLInterceptorimplementsInterceptor{@OverridepublicObject intercept(Invocation invocation)throwsThrowable {// invocation.getArgs()可以獲取到被攔截方法的參數(shù)// StatementHandler.update(Statement s)的參數(shù)為StatementStatement s = (Statement) invocation.getArgs()[0];// 數(shù)據(jù)源為DRUID, Statement為DRUID的StatementStatement stmt = ((DruidPooledPreparedStatement) s).getStatement();// 配置druid連接時使用filters: stat配置if(stmtinstanceofPreparedStatementProxyImpl) {? ? ? ? ? ? stmt = ((PreparedStatementProxyImpl) stmt).getRawObject();? ? ? ? }// 數(shù)據(jù)庫提供的Statement可獲取參數(shù)替換后的SQL(JDBC和DRUID獲取的是帶?的)// 數(shù)據(jù)庫為MySQL,可以直接強制轉(zhuǎn)換為MySQL的PreparedStatement獲取SQL// SQL在書寫時為了格式容器閱讀會有換行符(多個空格)存在// 為了保存和查看方便去除SQL中的換行及多個空格String sql = ((com.mysql.jdbc.PreparedStatement) stmt).asSql().replaceAll("\\s+"," ");// 保存SQL的操作必須和當(dāng)前執(zhí)行的SQL在同一事務(wù)中// 使用當(dāng)前SQL所在的數(shù)據(jù)庫連接執(zhí)行保存操作即可// 目標(biāo)sql成功時保存sql的方法也同步成功Connection conn = stmt.getConnection();// 將SQL保存至數(shù)據(jù)庫中PreparedStatement ps =null;try{? ? ? ? ? ? ps = conn.prepareStatement("INSERT INTO atd681_mybatis_sql (v_sql) VALUES (?)");? ? ? ? ? ? ps.setString(1, sql);// 因為和Mybatis的操作在同一事務(wù)中// 如果本次操作如果失敗, 所有操作都回滾ps.execute();? ? ? ? }finally{if(ps !=null) {? ? ? ? ? ? ? ? ps.close();? ? ? ? ? ? }? ? ? ? }// 繼續(xù)執(zhí)行StatementHandler.update方法returninvocation.proceed();? ? }}復(fù)制代碼

只有MySQL提供的PreparedStatement對象中可以獲取到最終的SQL.

保存SQL操作需要和Mybatis的操作在同一事務(wù)中, 必須同時成功或失敗.

5. 測試

在數(shù)據(jù)庫中創(chuàng)建兩張表:

atd681_mybatis_testatd681_mybatis_sql

創(chuàng)建?DAO?和?Mapper?, 創(chuàng)建增加, 刪除, 修改的方法及SQL

// 數(shù)據(jù)DAO@RepositorypublicinterfaceDataDAO{// 添加數(shù)據(jù)voidinsert(String dv);// 更新數(shù)據(jù)voidupdate(String dv);// 刪除數(shù)據(jù)voiddelete();}復(fù)制代碼

INSERT INTO atd681_mybatis_test ( dv ) VALUES ( #{dv})UPDATE atd681_mybatis_test1 SET dv = #{dv}DELETE FROM atd681_mybatis_test復(fù)制代碼

控制器中添加方法, 依次調(diào)用刪除, 添加, 更新. 保證三個操作在同一個事務(wù)中.

@RestControllerpublicclassDataController{// 注入DAO@AutowiredprivateDataDAO dao;// 分別執(zhí)行刪除,插入,更新操作// 參數(shù)i: 插入時的字符串// 參數(shù)u: 更新時的字符串@GetMapping("/mybatis/test")@TransactionalpublicString excuteSql(String i, String u) {// 刪除數(shù)據(jù)后將參數(shù)i的內(nèi)容插件數(shù)據(jù)庫,將數(shù)據(jù)更新成參數(shù)u的內(nèi)容// 該方法添加了事務(wù),3次數(shù)據(jù)庫操作會在同一個事務(wù)中執(zhí)行.// Mybatis攔截器會捕獲三次數(shù)據(jù)庫SQL插入至數(shù)據(jù)庫中(詳見攔截器)dao.delete();? ? ? ? dao.insert(i);? ? ? ? dao.update(u);return"success";? ? }}復(fù)制代碼

啟動服務(wù), 訪問?http://localhost:3456/mybatis/test?i=insert&u=update

程序依次執(zhí)行刪除、添加(內(nèi)容為?"insert"?)、更新(內(nèi)容為?"update"?)三個操作, 執(zhí)行完成后數(shù)據(jù)庫中有一條記錄(內(nèi)容為?"update"?). 由于配置了攔截器, 在每個操作執(zhí)行前將SQL保持至數(shù)據(jù)庫中, 因此三條SQL也被保存至數(shù)據(jù)庫中.

上述過程中除了3次業(yè)務(wù)操作, 還有3次保持SQL的操作, 因此數(shù)據(jù)庫總共會執(zhí)行6條SQL.

執(zhí)行DELETE操作

保存1中DELETE操作的SQL

執(zhí)行INSERT SQL

保存3中INSERT操作的SQL

執(zhí)行UPDATE SQL

保存5中UPDATE操作的SQL

上述6次數(shù)據(jù)庫操作必須在同一事務(wù)中, 否則一旦出現(xiàn)業(yè)務(wù)操作成功但保存SQL失敗的情況. 服務(wù)器端同步的數(shù)據(jù)就會與客戶端本地不一致.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. 簡介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存儲過程以及高級映射的優(yōu)秀的...
    笨鳥慢飛閱讀 6,248評論 0 4
  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 32,311評論 2 89
  • 流年如水,裊裊炊煙暮。 雞犬聲聲春又瘦,枝藏幾多春夢。 晨醒朝露嘟嘟,夜眠鶯語啼啼。 心樂無憂園畝,淡泊最是芳華。
    抱一閱讀 396評論 2 2
  • 孩子們早早起來,各自收拾著這次為期一周的行李,這次出發(fā)我不給你們整理東西,其因是我不跟著,我給你們整理好了,你們自...
    平安是福王愛云閱讀 225評論 0 0
  • 周四晚上一個人背著行李,與靈魂私奔坐著半夜火車到了沈陽,在沈陽的街頭走一走全然不顧垂暮已久的天空,深夜一點街...
    溺渡623閱讀 358評論 0 0

友情鏈接更多精彩內(nèi)容