SpringCloud或SpringBoot+Mybatis-Plus利用AOP+mybatis外掛實現資料操作記錄及更新對比

TopSkyhua發表於2020-07-16

引文

  本文主要介紹如何使用Spring AOP + mybatis外掛實現攔截資料庫操作並根據不同需求進行資料對比分析,主要適用於系統中需要對資料操作進行記錄、在更新資料時準確記錄更新欄位

核心:AOP、mybatis外掛(攔截器)、mybatis-Plus實體規範、資料對比

1、相關技術簡介

mybatis外掛

  mybatis外掛實際上就是官方針對4層資料操作處理預留的攔截器,使用者可以根據不同的需求進行操作攔截並處理。這邊筆者不做詳細描述,詳細介紹請到官網瞭解,這裡筆者就複用官網介紹。

外掛(plugins)

MyBatis 允許你在已對映語句執行過程中的某一點進行攔截呼叫。預設情況下,MyBatis 允許使用外掛來攔截的方法呼叫包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

這些類中方法的細節可以通過檢視每個方法的簽名來發現,或者直接檢視 MyBatis 發行包中的原始碼。 如果你想做的不僅僅是監控方法的呼叫,那麼你最好相當瞭解要重寫的方法的行為。 因為如果在試圖修改或重寫已有方法的行為的時候,你很可能在破壞 MyBatis 的核心模組。 這些都是更低層的類和方法,所以使用外掛的時候要特別當心。

通過 MyBatis 提供的強大機制,使用外掛是非常簡單的,只需實現 Interceptor 介面,並指定想要攔截的方法簽名即可。

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    Object returnObject = invocation.proceed();
    // implement post processing if need
    return returnObject;
  }
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}
<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

上面的外掛將會攔截在 Executor 例項中所有的 “update” 方法呼叫, 這裡的 Executor 是負責執行低層對映語句的內部物件。

提示 覆蓋配置類

除了用外掛來修改 MyBatis 核心行為之外,還可以通過完全覆蓋配置類來達到目的。只需繼承後覆蓋其中的每個方法,再把它傳遞到 SqlSessionFactoryBuilder.build(myConfig) 方法即可。再次重申,這可能會嚴重影響 MyBatis 的行為,務請慎之又慎。

 

重點講下4層處理,MyBatis兩級快取就是在其中兩層中實現

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) 
    • 所有資料庫操作到達底層後都由該執行器進行任務分發,主要有update(插入、更新、刪除),query(查詢),提交,回滾,關閉連結等
  • ParameterHandler (getParameterObject, setParameters) 
    • 引數處理器(獲取引數,設定引數)
  • ResultSetHandler (handleResultSets, handleOutputParameters) 
    • 結果集處理器(結果集,輸出引數)
  • StatementHandler (prepare, parameterize, batch, update, query) 
    • 宣告處理器、準備連結jdbc前處理,prepare(預處理):生成sql語句,準備連結資料庫進行操作

以上4層執行順序為順序執行

  • Executor是 Mybatis的內部執行器,它負責呼叫StatementHandler運算元據庫,並把結果集通過 ResultSetHandler進行自動對映,另外,他還處理了二級快取的操作。從這裡可以看出,我們也是可以通過外掛來實現自定義的二級快取的。
  • ParameterHandler是Mybatis實現Sql入參設定的物件。外掛可以改變我們Sql的引數預設設定。
  • ResultSetHandler是Mybatis把ResultSet集合對映成POJO的介面物件。我們可以定義外掛對Mybatis的結果集自動對映進行修改。
  • StatementHandler是Mybatis直接和資料庫執行sql指令碼的物件。另外它也實現了Mybatis的一級快取。這裡,我們可以使用外掛來實現對一級快取的操作(禁用等等)。

MyBatis-Plus:

  MyBatis增強器,主要規範了資料實體,在底層實現了簡單的增刪查改,使用者不再需要開發基礎操作介面,小編認為是最強大、最方便易用的,沒有之一,不接受任何反駁。詳細介紹請看官網

資料實體的規範讓底層操作更加便捷,本例主要實體規範中的表名以及主鍵獲取,下面上實體規範demo

package com.lith.datalog.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * <p>
 * 使用者表
 * </p>
 *
 * @author Tophua
 * @since 2020/5/7
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class User extends Model<User> {
    /**
     * 主鍵id
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    private String name;
    private Integer age;
    private String email;
}

2、實現

本文所要講述的就是在第一級(Executor)進行攔截並實現資料對比記錄。
本例為公共模組實現,然後在其它模組中依賴此公共模組,根據每個模組不同的需求自定義實現不同的處理。
結構目錄

一、配置

 1 package com.lith.datalog.config;
 2 
 3 import com.lith.datalog.handle.DataUpdateInterceptor;
 4 import org.mybatis.spring.annotation.MapperScan;
 5 import org.springframework.context.annotation.Bean;
 6 import org.springframework.context.annotation.Configuration;
 7 import org.springframework.context.annotation.Profile;
 8 import org.springframework.transaction.annotation.EnableTransactionManagement;
 9 
10 import javax.sql.DataSource;
11 
12 /**
13  * <p>
14  * Mybatis-Plus配置
15  * </p>
16  *
17  * @author Tophua
18  * @since 2020/5/7
19  */
20 @Configuration
21 @EnableTransactionManagement
22 @MapperScan("com.lith.**.mapper")
23 public class MybatisPlusConfig {
24 
25     /**
26      * <p>
27      * SQL執行效率外掛  設定 dev test 環境開啟
28      * </p>
29      *
30      * @return cn.rc100.common.data.mybatis.EplusPerformanceInterceptor
31      * @author Tophua
32      * @since 2020/3/11
33      */
34     @Bean
35     @Profile({"dev","test"})
36     public PerformanceInterceptor performanceInterceptor() {
37         return new PerformanceInterceptor();
38     }
39 
40     /**
41      * <p>
42      * 資料更新操作處理
43      * </p>
44      *
45      * @return com.lith.datalog.handle.DataUpdateInterceptor
46      * @author Tophua
47      * @since 2020/5/11
48      */
49     @Bean
50     @Profile({"dev","test"})
51     public DataUpdateInterceptor dataUpdateInterceptor(DataSource dataSource) {
52         return new DataUpdateInterceptor(dataSource);
53     }
54 }

二、實現攔截器

DataUpdateInterceptor,根據官網demo實現攔截器,在攔截器中根據增、刪、改操作去呼叫各個模組中自定義實現的處理方法來達到不同的操作處理。

  1 package com.lith.datalog.handle;
  2 
  3 import cn.hutool.db.Db;
  4 import com.baomidou.mybatisplus.core.metadata.TableInfo;
  5 import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
  6 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
  7 import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
  8 import com.baomidou.mybatisplus.core.toolkit.StringPool;
  9 import com.baomidou.mybatisplus.core.toolkit.TableNameParser;
 10 import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;
 11 import com.lith.datalog.aspect.DataLogAspect;
 12 import com.lith.datalog.aspect.DataTem;
 13 import lombok.AllArgsConstructor;
 14 import lombok.extern.slf4j.Slf4j;
 15 import org.apache.ibatis.executor.statement.StatementHandler;
 16 import org.apache.ibatis.mapping.MappedStatement;
 17 import org.apache.ibatis.mapping.SqlCommandType;
 18 import org.apache.ibatis.plugin.Interceptor;
 19 import org.apache.ibatis.plugin.Intercepts;
 20 import org.apache.ibatis.plugin.Invocation;
 21 import org.apache.ibatis.plugin.Signature;
 22 import org.apache.ibatis.reflection.MetaObject;
 23 import org.apache.ibatis.reflection.SystemMetaObject;
 24 
 25 import javax.sql.DataSource;
 26 import java.lang.reflect.Proxy;
 27 import java.sql.Statement;
 28 import java.util.*;
 29 
 30 /**
 31  * <p>
 32  * 資料更新攔截器
 33  * </p>
 34  *
 35  * @author Tophua
 36  * @since 2020/5/11
 37  */
 38 @Slf4j
 39 @AllArgsConstructor
 40 @Intercepts({@Signature(type = StatementHandler.class, method = "update", args = {Statement.class})})
 41 public class DataUpdateInterceptor extends AbstractSqlParserHandler implements Interceptor {
 42     private final DataSource dataSource;
 43 
 44     @Override
 45     public Object intercept(Invocation invocation) throws Throwable {
 46         // 獲取執行緒名,使用執行緒名作為同一次操作記錄
 47         String threadName = Thread.currentThread().getName();
 48         // 判斷是否需要記錄日誌
 49         if (!DataLogAspect.hasThread(threadName)) {
 50             return invocation.proceed();
 51         }
 52         Statement statement;
 53         Object firstArg = invocation.getArgs()[0];
 54         if (Proxy.isProxyClass(firstArg.getClass())) {
 55             statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement");
 56         } else {
 57             statement = (Statement) firstArg;
 58         }
 59         MetaObject stmtMetaObj = SystemMetaObject.forObject(statement);
 60         try {
 61             statement = (Statement) stmtMetaObj.getValue("stmt.statement");
 62         } catch (Exception e) {
 63             // do nothing
 64         }
 65         if (stmtMetaObj.hasGetter("delegate")) {
 66             //Hikari
 67             try {
 68                 statement = (Statement) stmtMetaObj.getValue("delegate");
 69             } catch (Exception ignored) {
 70 
 71             }
 72         }
 73 
 74         String originalSql = statement.toString();
 75         originalSql = originalSql.replaceAll("[\\s]+", StringPool.SPACE);
 76         int index = indexOfSqlStart(originalSql);
 77         if (index > 0) {
 78             originalSql = originalSql.substring(index);
 79         }
 80 
 81         StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
 82         MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
 83         this.sqlParser(metaObject);
 84         MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
 85 
 86         // 獲取執行Sql
 87         String sql = originalSql.replace("where", "WHERE");
 88         // 插入
 89         if (SqlCommandType.INSERT.equals(mappedStatement.getSqlCommandType())) {
 90         }
 91         // 更新
 92         if (SqlCommandType.UPDATE.equals(mappedStatement.getSqlCommandType())) {
 93             try {
 94                 // 使用mybatis-plus 工具解析sql獲取表名
 95                 Collection<String> tables = new TableNameParser(sql).tables();
 96                 if (CollectionUtils.isEmpty(tables)) {
 97                     return invocation.proceed();
 98                 }
 99                 String tableName = tables.iterator().next();
100                 // 使用mybatis-plus 工具根據表名找出對應的實體類
101                 Class<?> entityType = TableInfoHelper.getTableInfos().stream().filter(t -> t.getTableName().equals(tableName))
102                         .findFirst().orElse(new TableInfo(null)).getEntityType();
103 
104                 DataTem dataTem = new DataTem();
105                 dataTem.setTableName(tableName);
106                 dataTem.setEntityType(entityType);
107                 // 設定sql用於執行完後查詢新資料
108                 dataTem.setSql("SELECT * FROM " + tableName + " WHERE id in ");
109                 String selectSql = "SELECT * FROM " + tableName + " " + sql.substring(sql.lastIndexOf("WHERE"));
110                 // 查詢更新前資料
111                 List<?> oldData = Db.use(dataSource).query(selectSql, entityType);
112                 dataTem.setOldData(oldData);
113                 DataLogAspect.put(threadName, dataTem);
114             } catch (Exception e) {
115                 e.printStackTrace();
116             }
117         }
118         // 刪除
119         if (SqlCommandType.DELETE.equals(mappedStatement.getSqlCommandType())) {
120         }
121         return invocation.proceed();
122     }
123 
124     /**
125      * 獲取sql語句開頭部分
126      *
127      * @param sql ignore
128      * @return ignore
129      */
130     private int indexOfSqlStart(String sql) {
131         String upperCaseSql = sql.toUpperCase();
132         Set<Integer> set = new HashSet<>();
133         set.add(upperCaseSql.indexOf("SELECT "));
134         set.add(upperCaseSql.indexOf("UPDATE "));
135         set.add(upperCaseSql.indexOf("INSERT "));
136         set.add(upperCaseSql.indexOf("DELETE "));
137         set.remove(-1);
138         if (CollectionUtils.isEmpty(set)) {
139             return -1;
140         }
141         List<Integer> list = new ArrayList<>(set);
142         list.sort(Comparator.naturalOrder());
143         return list.get(0);
144     }
145 }

二、AOP

使用AOP主要是考慮到一個方法中會出現多次資料庫操作,而這些操作在記錄中只能算作使用者的一次操作,故使用AOP進行操作隔離,將一個方法內的所有資料庫操作合併為一次記錄。

此外AOP還代表著是否需要記錄日誌,有切點才會進行記錄。

AOP 切點註解

 1 package com.lith.datalog.annotation;
 2 
 3 import java.lang.annotation.ElementType;
 4 import java.lang.annotation.Retention;
 5 import java.lang.annotation.RetentionPolicy;
 6 import java.lang.annotation.Target;
 7 
 8 /**
 9  * <p>
10  * 資料日誌
11  * </p>
12  *
13  * @author Tophua
14  * @since 2020/7/15
15  */
16 @Target(ElementType.METHOD)
17 @Retention(RetentionPolicy.RUNTIME)
18 public @interface DataLog {
19 }

三、AOP切面實現

採用方法執行前後進行處理

  1 package com.lith.datalog.aspect;
  2 
  3 import cn.hutool.core.collection.CollUtil;
  4 import cn.hutool.core.util.ObjectUtil;
  5 import cn.hutool.core.util.StrUtil;
  6 import cn.hutool.db.Db;
  7 import cn.hutool.json.JSONUtil;
  8 import com.lith.datalog.annotation.DataLog;
  9 import com.lith.datalog.handle.CompareResult;
 10 import lombok.AllArgsConstructor;
 11 import lombok.SneakyThrows;
 12 import org.aspectj.lang.annotation.After;
 13 import org.aspectj.lang.annotation.Aspect;
 14 import org.aspectj.lang.annotation.Before;
 15 import org.springframework.core.annotation.Order;
 16 import org.springframework.scheduling.annotation.Async;
 17 import org.springframework.stereotype.Component;
 18 
 19 import javax.sql.DataSource;
 20 import java.lang.reflect.Field;
 21 import java.lang.reflect.Method;
 22 import java.sql.SQLException;
 23 import java.util.*;
 24 import java.util.concurrent.ConcurrentHashMap;
 25 import java.util.stream.Collectors;
 26 
 27 /**
 28  * <p>
 29  * DataLog切面
 30  * </p>
 31  *
 32  * @author Tophua
 33  * @since 2020/7/15
 34  */
 35 @Aspect
 36 @Order(99)
 37 @Component
 38 @AllArgsConstructor
 39 public class DataLogAspect {
 40 
 41     private final DataSource dataSource;
 42 
 43     private static final Map<String, List<DataTem>> TEM_MAP = new ConcurrentHashMap<>();
 44 
 45     /**
 46      * <p>
 47      * 判斷執行緒是否需要記錄日誌
 48      * </p>
 49      *
 50      * @param threadName threadName
 51      * @return boolean
 52      * @author Tophua
 53      * @since 2020/7/15
 54      */
 55     public static boolean hasThread(String threadName) {
 56         return TEM_MAP.containsKey(threadName);
 57     }
 58 
 59     /**
 60      * <p>
 61      * 增加執行緒資料庫操作
 62      * </p>
 63      *
 64      * @param threadName threadName
 65      * @param dataTem dataTem
 66      * @return void
 67      * @author Tophua
 68      * @since 2020/7/15
 69      */
 70     public static void put(String threadName, DataTem dataTem) {
 71         if (TEM_MAP.containsKey(threadName)) {
 72             TEM_MAP.get(threadName).add(dataTem);
 73         }
 74     }
 75 
 76     /**
 77      * <p>
 78      * 切面前執行
 79      * </p>
 80      *
 81      * @param dataLog dataLog
 82      * @return void
 83      * @author Tophua
 84      * @since 2020/7/15
 85      */
 86     @SneakyThrows
 87     @Before("@annotation(dataLog)")
 88     public void before(DataLog dataLog) {
 89         // 獲取執行緒名,使用執行緒名作為同一次操作記錄
 90         String threadName = Thread.currentThread().getName();
 91         TEM_MAP.put(threadName, new LinkedList<>());
 92     }
 93 
 94     /**
 95      * <p>
 96      * 切面後執行
 97      * </p>
 98      *
 99      * @param dataLog dataLog
100      * @return void
101      * @author Tophua
102      * @since 2020/7/15
103      */
104     @SneakyThrows
105     @After("@annotation(dataLog)")
106     public void after(DataLog dataLog) {
107         // 獲取執行緒名,使用執行緒名作為同一次操作記錄
108         String threadName = Thread.currentThread().getName();
109         List<DataTem> list = TEM_MAP.get(threadName);
110         if (CollUtil.isEmpty(list)) {
111             return;
112         }
113         list.forEach(dataTem -> {
114             List<?> oldData = dataTem.getOldData();
115             if (CollUtil.isEmpty(oldData)) {
116                 return;
117             }
118             String ids = oldData.stream().map(o -> {
119                 try {
120                     Method method = o.getClass().getMethod("getId");
121                     return method.invoke(o).toString();
122                 } catch (Exception e) {
123                     e.printStackTrace();
124                     return null;
125                 }
126             }).filter(ObjectUtil::isNotNull).collect(Collectors.joining(","));
127             String sql = dataTem.getSql() + "(" + ids + ")";
128             try {
129                 List<?> newData = Db.use(dataSource).query(sql, dataTem.getEntityType());
130                 dataTem.setNewData(newData);
131                 System.out.println("oldData:" + JSONUtil.toJsonStr(dataTem.getOldData()));
132                 System.out.println("newData:" + JSONUtil.toJsonStr(dataTem.getNewData()));
133 
134             } catch (SQLException e) {
135                 e.printStackTrace();
136             }
137         });
138         // 非同步對比存庫
139         this.compareAndSave(list);
140     }
141 
142     /**
143      * <p>
144      * 對比儲存
145      * </p>
146      *
147      * @param list list
148      * @return void
149      * @author Tophua
150      * @since 2020/7/15
151      */
152     @Async
153     public void compareAndSave(List<DataTem> list) {
154         StringBuilder sb = new StringBuilder();
155         list.forEach(dataTem -> {
156             List<?> oldData = dataTem.getOldData();
157             List<?> newData = dataTem.getNewData();
158             // 按id排序
159             oldData.sort(Comparator.comparingLong(d -> {
160                 try {
161                     Method method = d.getClass().getMethod("getId");
162                     return Long.parseLong(method.invoke(d).toString());
163                 } catch (Exception e) {
164                     e.printStackTrace();
165                 }
166                 return 0L;
167             }));
168             newData.sort(Comparator.comparingLong(d -> {
169                 try {
170                     Method method = d.getClass().getMethod("getId");
171                     return Long.parseLong(method.invoke(d).toString());
172                 } catch (Exception e) {
173                     e.printStackTrace();
174                 }
175                 return 0L;
176             }));
177 
178             for (int i = 0; i < oldData.size(); i++) {
179                 final int[] finalI = {0};
180                 sameClazzDiff(oldData.get(i), newData.get(i)).forEach(r -> {
181                     if (finalI[0] == 0) {
182                         sb.append(StrUtil.LF);
183                         sb.append(StrUtil.format("修改表:【{}】", dataTem.getTableName()));
184                         sb.append(StrUtil.format("id:【{}】", r.getId()));
185                     }
186                     sb.append(StrUtil.LF);
187                     sb.append(StrUtil.format("把欄位[{}]從[{}]改為[{}]", r.getFieldName(), r.getOldValue(), r.getNewValue()));
188                     finalI[0]++;
189                 });
190             }
191         });
192         if (sb.length() > 0) {
193             sb.deleteCharAt(0);
194         }
195         // 存庫
196         System.err.println(sb.toString());
197     }
198 
199     /**
200      * <p>
201      * 相同類對比
202      * </p>
203      *
204      * @param obj1 obj1
205      * @param obj2 obj2
206      * @return java.util.List<com.lith.datalog.handle.CompareResult>
207      * @author Tophua
208      * @since 2020/7/15
209      */
210     private List<CompareResult> sameClazzDiff(Object obj1, Object obj2) {
211         List<CompareResult> results = new ArrayList<>();
212         Field[] obj1Fields = obj1.getClass().getDeclaredFields();
213         Field[] obj2Fields = obj2.getClass().getDeclaredFields();
214         Long id = null;
215         for (int i = 0; i < obj1Fields.length; i++) {
216             obj1Fields[i].setAccessible(true);
217             obj2Fields[i].setAccessible(true);
218             Field field = obj1Fields[i];
219             try {
220                 Object value1 = obj1Fields[i].get(obj1);
221                 Object value2 = obj2Fields[i].get(obj2);
222                 if ("id".equals(field.getName())) {
223                     id = Long.parseLong(value1.toString());
224                 }
225                 if (!ObjectUtil.equal(value1, value2)) {
226                     CompareResult r = new CompareResult();
227                     r.setId(id);
228                     r.setFieldName(field.getName());
229                     // 獲取註釋
230                     r.setFieldComment(field.getName());
231                     r.setOldValue(value1);
232                     r.setNewValue(value2);
233                     results.add(r);
234                 }
235             } catch (IllegalAccessException e) {
236                 e.printStackTrace();
237             }
238         }
239         return results;
240     }
241 
242 }

3、測試及結果

經過測試,不管怎麼使用資料更新操作,結果都可以進行攔截記錄,完美達到預期。

小筆這裡並沒有將記錄儲存在資料庫,由大家自行儲存。

測試demo

 1 package com.lith.datalog.controller;
 2 
 3 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 4 import com.lith.datalog.annotation.DataLog;
 5 import com.lith.datalog.entity.User;
 6 import com.lith.datalog.mapper.UserMapper;
 7 import com.lith.datalog.service.UserService;
 8 import lombok.AllArgsConstructor;
 9 import org.springframework.transaction.annotation.Transactional;
10 import org.springframework.web.bind.annotation.*;
11 
12 /**
13  * <p>
14  * UserController
15  * </p>
16  *
17  * @author Tophua
18  * @since 2020/5/7
19  */
20 @RestController
21 @AllArgsConstructor
22 @RequestMapping("/user")
23 public class UserController {
24 
25     private final UserService userService;
26     private final UserMapper userMapper;
27 
28     @GetMapping("{id}")
29     public User getById(@PathVariable Integer id) {
30         return userService.getById(id);
31     }
32 
33     @DataLog
34     @PostMapping
35     public Boolean save(@RequestBody User user) {
36         return userService.save(user);
37     }
38 
39     @DataLog
40     @PutMapping
41     @Transactional(rollbackFor = Exception.class)
42     public Boolean updateById(@RequestBody User user) {
43         User nUser = new User();
44         nUser.setId(2);
45         nUser.setName("程式碼更新");
46         nUser.updateById();
47         userService.update(Wrappers.<User>lambdaUpdate()
48                 .set(User::getName, "批量")
49                 .in(User::getId, 3, 4));
50         userMapper.updateTest();
51         return userService.updateById(user);
52     }
53 
54     @DeleteMapping("{id}")
55     public Boolean removeById(@PathVariable Integer id) {
56         return userService.removeById(id);
57     }
58 }

結果顯示:

Time:2 ms - ID:com.lith.datalog.mapper.UserMapper.updateById
Execute SQL:UPDATE user SET name='程式碼更新' WHERE id=2

 Time:2 ms - ID:com.lith.datalog.mapper.UserMapper.update
Execute SQL:UPDATE user SET name='批量' WHERE (id IN (3,4))

 Time:2 ms - ID:com.lith.datalog.mapper.UserMapper.updateTest
Execute SQL:update user set age = 44 where id in (5,6)

 Time:0 ms - ID:com.lith.datalog.mapper.UserMapper.updateById
Execute SQL:UPDATE user SET name='4564', age=20, email='dsahkdhkashk' WHERE id=1

oldData:[{"name":"1","id":2,"age":10,"email":"dsahkdhkashk"}]
newData:[{"name":"程式碼更新","id":2,"age":10,"email":"dsahkdhkashk"}]
oldData:[{"name":"1","id":3,"age":10,"email":"dsahkdhkashk"},{"name":"1","id":4,"age":10,"email":"dsahkdhkashk"}]
newData:[{"name":"批量","id":3,"age":10,"email":"dsahkdhkashk"},{"name":"批量","id":4,"age":10,"email":"dsahkdhkashk"}]
oldData:[{"name":"1","id":5,"age":10,"email":"dsahkdhkashk"},{"name":"1","id":6,"age":10,"email":"dsahkdhkashk"}]
newData:[{"name":"1","id":5,"age":44,"email":"dsahkdhkashk"},{"name":"1","id":6,"age":44,"email":"dsahkdhkashk"}]
oldData:[{"name":"1","id":1,"age":10,"email":"dsahkdhkashk"}]
newData:[{"name":"4564","id":1,"age":20,"email":"dsahkdhkashk"}]
修改表:【user】id:【2】
把欄位[name]從[1]改為[程式碼更新]
修改表:【user】id:【3】
把欄位[name]從[1]改為[批量]
修改表:【user】id:【4】
把欄位[name]從[1]改為[批量]
修改表:【user】id:【5】
把欄位[age]從[10]改為[44]
修改表:【user】id:【6】
把欄位[age]從[10]改為[44]
修改表:【user】id:【1】
把欄位[name]從[1]改為[4564]
把欄位[age]從[10]改為[20]

4、總結

  本次綜合前車經驗,優化設計思想,改為從底層具體執行的 sql 語句入手,通過解析表名及更新條件來構造資料更新前後的查詢sql,再使用Spring AOP對方法執行前後進行處理,記錄更新前後的資料。最後再使用java反射機制將資料更新前後進行對比記錄。

注:

使用AOP涉及到一點,就是需要保證AOP與Spring 資料庫事務之間的執行順序,如果AOP先執行然後再提交事務,那結果則是資料無變化。

在此小筆已將AOP處理級別放到最後,保證先提交事務再去查詢更新後的資料,這樣才能得出正確的結果。

 

歡迎各路大神交流意見。。。。。。

 

最後附上原始碼地址:

https://gitee.com/TopSkyhua/datalog

相關文章