作者:青石路
來源:https://www.cnblogs.com/youzhibing/p/18019399
MyBatis 替換成 MyBatis-Plus
背景介紹
一個老專案,資料庫用的是 MySQL 5.7.36 , ORM 框架用的 MyBatis 3.5.0 , mysql-connector-java 版本是 5.1.26
新來了一個幹練的小夥,精力充沛,看著就是一個喜歡折騰的主
他就覺得 MyBatis 使用起來不夠簡單,要寫的程式碼還比較多,覺得有必要替換成 MyBatis-Plus
Mybatis-Plus 替換 Mybatis
先準備一張表 tbl_order ,然後初始化 2 條資料
DROP TABLE IF EXISTS `tbl_order`;
CREATE TABLE `tbl_order` (
`id` bigint(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
`order_no` varchar(50) NOT NULL COMMENT '訂單號',
`pay_time` datetime(3) DEFAULT NULL COMMENT '付款時間',
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '建立時間',
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最終修改時間',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB COMMENT = '訂單';
INSERT INTO `tbl_order` VALUES (1, '123456', '2024-02-21 18:38:32.000', '2024-02-21 18:37:34.000', '2024-02-21 18:40:01.720');
INSERT INTO `tbl_order` VALUES (2, '654321', '2024-02-21 19:33:32.000','2024-02-21 19:32:12.020', '2024-02-21 19:34:03.727');
推薦一個開源免費的 Spring Boot 實戰專案:
https://github.com/javastacks/spring-boot-best-practice
為了簡化演示,我就直接用 Mybatis-Plus 搭建一個示例 demo ,以此來模擬下 "小夥" 替換的過程
只是用 MyBatis-Plus 替換 MyBatis ,其他元件的版本暫不動
Mybatis-Plus 版本就用 "小夥" 引用的版本:3.1.1 , mysql-connector-java 版本保持不變還是 5.1.26
示例程式碼:play_it_safe
此時執行 com.qsl.OrderTest#orderListAllTest ,會報錯,異常資訊如下
org.springframework.dao.TransientDataAccessResourceException: Error attempting to get column 'pay_time' from result set. Cause: java.sql.SQLException: Conversion not supported for type java.time.LocalDateTime
; Conversion not supported for type java.time.LocalDateTime; nested exception is java.sql.SQLException: Conversion not supported for type java.time.LocalDateTime
at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:110)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
at com.sun.proxy.$Proxy53.selectList(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.executeForMany(MybatisMapperMethod.java:158)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:76)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:62)
at com.sun.proxy.$Proxy59.selectList(Unknown Source)
at com.qsl.OrderTest.orderListAllTest(OrderTest.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
Caused by: java.sql.SQLException: Conversion not supported for type java.time.LocalDateTime
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1078)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:989)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:975)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:920)
at com.mysql.jdbc.ResultSetImpl.getObject(ResultSetImpl.java:5126)
at com.mysql.jdbc.JDBC4ResultSet.getObject(JDBC4ResultSet.java:547)
at com.mysql.jdbc.ResultSetImpl.getObject(ResultSetImpl.java:5133)
at com.zaxxer.hikari.pool.HikariProxyResultSet.getObject(HikariProxyResultSet.java)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.ibatis.logging.jdbc.ResultSetLogger.invoke(ResultSetLogger.java:69)
at com.sun.proxy.$Proxy71.getObject(Unknown Source)
at org.apache.ibatis.type.LocalDateTimeTypeHandler.getNullableResult(LocalDateTimeTypeHandler.java:38)
at org.apache.ibatis.type.LocalDateTimeTypeHandler.getNullableResult(LocalDateTimeTypeHandler.java:28)
at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:81)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyAutomaticMappings(DefaultResultSetHandler.java:521)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:402)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:354)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:328)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:301)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:194)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:65)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
at com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor.doQuery(MybatisSimpleExecutor.java:67)
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:83)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
... 39 more
注意看 Caused by
不支援的轉換型別:java.time.LocalDateTime
誰不支援?mysql-connector-java 不支援!
那 mysql-connector-java 哪個版本支援了,答案是:5.1.37
升級mysql-connector-java
將 mysql-connector-java 升級到 5.1.37 ,再執行下 com.qsl.OrderTest#orderListAllTest
不再報異常,查詢結果也正確
MyBatis-Plus 替換 Mybatis 似乎就完成了
順的讓人有點懷疑
Conversion not supported for type java.time.LocalDateTime
我們再回過頭去看看前面說到的異常:Conversion not supported for type java.time.LocalDateTime
Mybatis-Plus 替換 MyBatis 之前沒這個異常,替換之後就有了這個異常,這不是 Mybatis-Plus 的問題?
如何找這個異常的根因了?
很簡單,直接從異常堆疊入手
點了之後,你會發現方法很簡單
這麼簡單的程式碼能有什麼問題?
大家注意看圖中左上角 MyBatis 的版本,是 3.5.1,並不是最初的 3.5.0
有小夥伴可能會問了:不是用 MyBatis-Plus 替換了 MyBatis 嗎,怎麼還有 Mybatis ?
這個問題問的真的好,我只想給你個大嘴巴子
你看下 MyBatis-Plus 的官方說明
既然基於 Mybatis 3.5.0 沒有拋異常,而基於 3.5.1 拋了異常, LocalDateTimeTypeHandler 在 3.5.1 肯定做了調整
我們來看下調整了什麼?
看出什麼了?
MyBatis 3.5.0 會處理 LocalDateTime 型別的轉換(將 java.sql.Timestamp 轉換成 java.time.LocalDateTime )
然而,注意了,然而來了!!!
然而從 MyBatis 3.5.1 開始,不再處理 LocalDateTime (還包括:LocalDate 、 LocalTime )型別的轉換
而是交由 JDBC 元件,也就是 mysql-connector-java 來實現
而巧的是, mysql-connector-java 5.1.26 不支援型別 LocalDateTime
那它支援哪些型別了?
我們同樣從異常堆疊入手
點了之後,可以看到下圖
public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
if (type == null) {
throw SQLError.createSQLException("Type parameter can not be null",
SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
}
if (type.equals(String.class)) {
return (T) getString(columnIndex);
} else if (type.equals(BigDecimal.class)) {
return (T) getBigDecimal(columnIndex);
} else if (type.equals(Boolean.class) || type.equals(Boolean.TYPE)) {
return (T) Boolean.valueOf(getBoolean(columnIndex));
} else if (type.equals(Integer.class) || type.equals(Integer.TYPE)) {
return (T) Integer.valueOf(getInt(columnIndex));
} else if (type.equals(Long.class) || type.equals(Long.TYPE)) {
return (T) Long.valueOf(getLong(columnIndex));
} else if (type.equals(Float.class) || type.equals(Float.TYPE)) {
return (T) Float.valueOf(getFloat(columnIndex));
} else if (type.equals(Double.class) || type.equals(Double.TYPE)) {
return (T) Double.valueOf(getDouble(columnIndex));
} else if (type.equals(byte[].class)) {
return (T) getBytes(columnIndex);
} else if (type.equals(java.sql.Date.class)) {
return (T) getDate(columnIndex);
} else if (type.equals(Time.class)) {
return (T) getTime(columnIndex);
} else if (type.equals(Timestamp.class)) {
return (T) getTimestamp(columnIndex);
} else if (type.equals(Clob.class)) {
return (T) getClob(columnIndex);
} else if (type.equals(Blob.class)) {
return (T) getBlob(columnIndex);
} else if (type.equals(Array.class)) {
return (T) getArray(columnIndex);
} else if (type.equals(Ref.class)) {
return (T) getRef(columnIndex);
} else if (type.equals(URL.class)) {
return (T) getURL(columnIndex);
// } else if (type.equals(Struct.class)) {
//
// }
// } else if (type.equals(RowId.class)) {
//
// } else if (type.equals(NClob.class)) {
//
// } else if (type.equals(SQLXML.class)) {
} else {
if (this.connection.getAutoDeserialize()) {
try {
return (T) getObject(columnIndex);
} catch (ClassCastException cce) {
SQLException sqlEx = SQLError.createSQLException("Conversion not supported for type " + type.getName(),
SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
sqlEx.initCause(cce);
throw sqlEx;
}
}
throw SQLError.createSQLException("Conversion not supported for type " + type.getName(),
SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
}
}
確實沒有 LocalDateTime 、 LocalDate 和 LocalTime
mysql-connector-java 5.1.37 開始支援 LocalDateTime 、 LocalDate 和 LocalTime ,前面已經介紹過了,不再過多贅述
總結下異常根因:MyBatis 3.5.1 開始不再處理 LocalDateTime 、 LocalDate 和 LocalTime 的轉換,而 mysql-connector-java 5.1.37 之前都不支援這些型別
弄清楚這個異常的來龍去脈之後,順的是不是又理所當然一些了?
暴風雨的來臨
版本上線沒 2 天,該來的終究還是來了
我們往表 tbl_order 中插入一條記錄:INSERT INTO tbl_order
VALUES (3, 'asdfgh', NULL, '2024-02-21 20:01:31.111', '2024-02-21 20:02:56.764');
再執行 com.qsl.OrderTest#orderListAllTest
此刻我就想問 "小夥" :刺不刺激?
碰到了異常,那就找原因
同樣從異常堆疊入手
看出什麼了?
如果 getTimestamp(columnIndex) 得到的是 NULL ,不就 NullPointerException ?嚴謹性了?
修復問題要緊,我們先看哪個版本進行修復了?
將 mysql-connector-java 升級到 5.1.42
問題得以修復
經此一役, "小夥" 似乎成長了很多,但眼裡的光卻暗淡了不少
mybatis-plus-issues-1114
無意中看到了這個issue-1114,跟我們前面分析的 Conversion not supported for type java.time.LocalDateTime 是不是同一個問題?
只是我們用到的資料庫連線池是預設的 HikariCP 而非 Druid
結合druid/issues/3302來看,如果使用 Druid 作為資料庫連線池,出現的異常可能跟我們前面分析的確實不一樣
所以大家需要根據自己的實際情況來分析,但針對異常的分析方法是通用的
修了“不該修的Bug”
這是我親身經歷的一次事故,到現在都覺得這鍋背的有點冤
背景介紹
檔案分為主檔案和附屬檔案,主檔案生成之後再生成附屬檔案
附屬檔案生成的時候,會校驗其依賴的主檔案是否都生成了,如果有任意一個主檔案未生成,依賴檔案不能生成並丟擲異常
這個業務還是比較簡單吧
但在附屬檔案校驗的最佳化上,我背上了生產事故
最佳化前的校驗
listFileGenerateLog 作用是根據引數查詢檔案生成記錄,具體實現不用關注
這個校驗邏輯是什麼?只要有任意一個主檔案生成,校驗就算透過了,與業務要求(主檔案全部生成,才算校驗透過)不匹配呀
這不是妥妥的 Bug ?
最佳化後的校驗
碰到 Bug 你能忍?我是忍不了一點,反手就是一個最佳化
這是不是就符合業務要求了?
生產異常
中午升級之後,穩定執行了一段時間,期間檔案正常生成,沒出現任何問題
晚上 19 點,有個附屬檔案生成失敗,異常提示:依賴的資源[abc_{yyyyMMdd}.txt]未生成
當時看到這個異常的第一眼,覺得既熟悉又陌生,熟悉的是這個異常資訊的結構,陌生的是 abc_{yyyyMMdd}.txt ,這不是檔名嗎?
正常來講應該是 fileId ,是一個自增的正整數呀,怎麼會是檔名了?
腦中瞬間閃過一個念頭:資料庫資料有問題?
一查嚇一跳,這個附屬檔案關聯主檔案的欄位值是:4356,abc_{yyyyMMdd}.txt ,看最終修改時間是:2021-08-21 15:22:12.652
4356 檔案的檔名就是 abc_{yyyyMMdd}.txt ,正常來講,這個關聯欄位的值應該是:4356
敢情這個 校驗Bug 完美的相容了這個髒資料 ,所以幾年了,一直沒出現異常
是不是有這味了?
這可倒好,我把 Bug 修好,還出現問題了,你說我是不是手賤?
經此一役,我眼裡的光又暗淡了些許
總結
關於對元件的升級,或者對舊程式碼的調整,都有可能牽一髮動全身,影響甚大
我的觀點是:能不動就不要動,改好沒績效,改出問題要背鍋,吃力不討好,又不是不能跑
如果到了不得不改的地步了,那就需要全面的測試。
更多文章推薦:
1.Spring Boot 3.x 教程,太全了!
2.2,000+ 道 Java面試題及答案整理(2024最新版)
3.免費獲取 IDEA 啟用碼的 7 種方式(2024最新版)
覺得不錯,別忘了隨手點贊+轉發哦!