MyBatisPlus TypeHandler自定義字段類型轉換Handler
前言
今天遇上這樣的一個情況,數據庫類型與Java對象屬性類型不對應,這種情況該如何處理
在 MySQL 中,字段的屬性為 BigInt
,按道理來說,對應Java
中的Long
類型。
但實際上項目中與之對應的 Java對象中的屬性的類型是 Date
類型,直接給我這個廢物當頭一棒
而且不是一兩張表,是比較多的表處於Date
和 BigInt
混用的情況,
你說要好好用Date就好好用Date,要好好用時間戳就好好用時間戳啊,還混用,類型還不對應,麻瞭
(別問這個項目怎麼出現這種事情的,就是來瞭人,又走瞭人,然後填坑)
保持微笑😀(此處口吐芬芳xxxxxx)
一、思考
我想知道出現這種情況,你是如何思考的?
我的思考是,到底是改數據庫,還是改程序代碼比較好。
但是無論哪一種我都不敢輕舉妄動,所以我做的第一步是把數據庫和代碼備份,確保不會被玩壞。
我也問瞭同事,他的建議是讓我改程序。
但是怎麼說勒,我細細比較瞭改代碼和改程序的麻煩程度,改數據表麻煩會少很多,我就在表結構中的Bigint 類型改為 datatime 類型,而且當時我的任務,是隻局限於一兩張業務表,影響范圍不大,引用也不多。
我就興沖沖的把表結構改瞭,然後把任務完成瞭~
等到今天上午,我之前詢問的那個同事也遇到這個問題,他就向上面的經理提瞭一嘴,說時間類型不對,問他標準是哪一種,經理說是時間戳,我心裡一涼~,麻瞭,(此處省略一萬句)
聽完,我就苦逼的把表結構改回來瞭,此時備份就發生作用瞭~
還原完數據表後,我就打算去改程序代碼瞭
周一寫 bug,bug 改一周
突然他和我聊到,xxx,你知道MybatisPlus,有什麼方法可以做這種轉換嗎?
這每一個都要改,太麻煩瞭,而且業務代碼中肯定也用到瞭,這改起來代價太大瞭,有沒有註解的方式可以解決轉換問題。
很淺顯的思考,但是我能夠感覺到自己的經驗的不足,對於很多偷懶(思考),我還是差的太遠瞭。
二、解決方式
因為用到的 ORM 框架是 MybatisPlus,所以首先找的就是有沒有官方的支持。
繼而就在官網找到一個字段類型處理器,一看才發現,是學過的東西啊,隻怪用的太少,知道的太少啊。
然後根據這個線索繼續找,就瞭解到 MyBatis-Plus 字段類型處理器 TypeHandler
這個 TypeHandler 處於的位置,就是應用程序和數據庫之間的攔截器,所有的操作,都會走一遍這裡。
就翻看源碼,想用一個東西,最快的方式就是看一下源碼的實現
2.1、TypeHandler源碼
public interface TypeHandler<T> { /** * 入庫前的類型轉換 */ void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; /** * 得到結果。 * 查詢後的數據處理 */ T getResult(ResultSet rs, String columnName) throws SQLException; T getResult(ResultSet rs, int columnIndex) throws SQLException; T getResult(CallableStatement cs, int columnIndex) throws SQLException; }
找到接口,看一下源碼中針對已有屬性是如何處理,我們仿寫一份,達到我們的要求即可啊.
2.2、BaseTypeHandler 源碼
有這麼多,我們直接看一下 BaseTypeHandler
是什麼樣的處理邏輯,
一方面 base 嗎,基礎嗎,我們就看看基礎是什麼樣的處理啦,另外一方面他是抽象類嗎,說明它其他實現類的基類嗎。
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> { @Override public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { if (parameter == null) { if (jdbcType == null) { throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters."); } try { ps.setNull(i, jdbcType.TYPE_CODE); } catch (SQLException e) { throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . " + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. " + "Cause: " + e, e); } } else { try { setNonNullParameter(ps, i, parameter, jdbcType); } catch (Exception e) { throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . " + "Try setting a different JdbcType for this parameter or a different configuration property. " + "Cause: " + e, e); } } } @Override public T getResult(ResultSet rs, String columnName) throws SQLException { try { return getNullableResult(rs, columnName); } catch (Exception e) { throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set. Cause: " + e, e); } } @Override public T getResult(ResultSet rs, int columnIndex) throws SQLException { try { return getNullableResult(rs, columnIndex); } catch (Exception e) { throw new ResultMapException("Error attempting to get column #" + columnIndex + " from result set. Cause: " + e, e); } } @Override public T getResult(CallableStatement cs, int columnIndex) throws SQLException { try { return getNullableResult(cs, columnIndex); } catch (Exception e) { throw new ResultMapException("Error attempting to get column #" + columnIndex + " from callable statement. Cause: " + e, e); } } // 這裡就是設置為 不為 null 時的入庫 public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; /** * 獲取可為空的結果。 */ public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException; public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException; public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException; }
看起來好像很長很多的樣子:當我們去掉那些判斷,精簡一下:
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> { @Override public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { // 設置不為null的參數,進行入庫 ,此處是抽象類,下層還有實現類, // 記住這裡,待會帶你看實現類,你就知道瞭 setNonNullParameter(ps, i, parameter, jdbcType); } @Override public T getResult(ResultSet rs, String columnName) throws SQLException { // 這裡從數據庫中獲取到數據,然後進行類型的一個設置 return getNullableResult(rs, columnName); } @Override public T getResult(ResultSet rs, int columnIndex) throws SQLException { //這兩個抽象方法,給我的感覺是一模一樣的,包括下一個也是如此 return getNullableResult(rs, columnIndex); } @Override public T getResult(CallableStatement cs, int columnIndex) throws SQLException { return getNullableResult(cs, columnIndex); } }
2.3、BigIntegerTypeHandler 源碼中的實現類
public class BigIntegerTypeHandler extends BaseTypeHandler<BigInteger> { @Override public void setNonNullParameter(PreparedStatement ps, int i, BigInteger parameter, JdbcType jdbcType) throws SQLException { // 這裡是轉為 BigDecimal ,所以這裡就算 setBigDecimal, // 那麼我們就可以猜測,它還支持其他的方法,Date的話,那就是setDate ps.setBigDecimal(i, new BigDecimal(parameter)); } @Override public BigInteger getNullableResult(ResultSet rs, String columnName) throws SQLException { BigDecimal bigDecimal = rs.getBigDecimal(columnName); // 這裡是rs.getBigDecimal ,我們待會去試一下能否getDate就可以瞭 return bigDecimal == null ? null : bigDecimal.toBigInteger(); } // 這兩個暫時沒有做瞭解,Debug的時候,斷點沒有執行到這,後期再補一塊的知識 // 但是為瞭以防萬一,我們待會也會照著它的方式將代碼改成這樣 @Override public BigInteger getNullableResult(ResultSet rs, int columnIndex) throws SQLException { BigDecimal bigDecimal = rs.getBigDecimal(columnIndex); return bigDecimal == null ? null : bigDecimal.toBigInteger(); } @Override public BigInteger getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { BigDecimal bigDecimal = cs.getBigDecimal(columnIndex); return bigDecimal == null ? null : bigDecimal.toBigInteger(); } }
這個實現類,沒什麼代碼,而且就是set、get ,並沒有其他的一些處理邏輯什麼的。
那麼我們也照這樣的方式實現一個。
2.4、嘗試
先明確目標,我們Mysql 中的字段類型 為 BigInt
,Java程序中的屬性類型為 Date
,
所以我們在入庫的時候就是要將 Date 類型轉化為 Long
進行入庫,
在從數據庫中取出來的時候,要從 Long
類型轉化為 Date
映射到 JavaBean中
我們直接copy上面的代碼,然後進行一些更改
public class MyDateTypeHandler implements TypeHandler<Date>{ /** * 入庫前的類型轉換 即執行insert、update方法時會執行 */ @Override public void setParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException { log.info("setParameter(PreparedStatement ps, int i, Date parameter,JdbcType jdbcType)...."); log.info("[{}],[{}]",parameter,jdbcType); ps.setLong(i, parameter.getTime()); } /** * 查詢後的數據處理 * 這就是查詢出來,進行映射的時候,會執行這段代碼 */ @Override public Date getResult(ResultSet rs, String columnName) throws SQLException { log.info("getResult(ResultSet rs, String columnName)....",columnName); return new Date(rs.getLong(columnName)); } @Override public Date getResult(ResultSet rs, int columnIndex) throws SQLException { log.info("getResult(ResultSet rs, int columnIndex)...."); return new Date(rs.getLong(columnIndex)); } @Override public Date getResult(CallableStatement cs, int columnIndex) throws SQLException { log.info("getResult(CallableStatement cs, int columnIndex)...."); return cs.getDate(columnIndex); } }
咋一眼好像成功啦,但是我們忽略瞭一個問題,就是MybatisPlus怎麼知道它的存在?
那些默認允許進行相互進行類型轉換的Handler,它在程序啟動的時候,就已經被註冊瞭。
但是我們寫瞭這個類,一方面沒有被MybatisPlus知曉,另一方面還沒有指明給誰使用,我們又該怎麼使用?
基於此,我寫瞭一個小Demo,希望大傢能夠弄明白,以後遇上也能夠解決一些問題
三、實踐案例
實現目標:
Mysql 中的表的字段為Bigint
,Java程序中為 Date 類型,我們希望還是可以一如既往的使用
MybatisPlus的方法,實現save、list類似這種方法的正常調用,而無需我在保存的時候,將前端傳過來的數據手動轉換為時間戳,再存放至數據庫。查詢時亦是如此
3.1、數據庫
數據庫
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for handler_test -- ---------------------------- DROP TABLE IF EXISTS `handler_test`; CREATE TABLE `handler_test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `date` bigint(50) NOT NULL COMMENT '存時間戳', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of handler_test -- ---------------------------- INSERT INTO `handler_test` VALUES (1, '測試數據1', 1659967236); INSERT INTO `handler_test` VALUES (2, '測試數據2', 1659967236); INSERT INTO `handler_test` VALUES (3, '測試插入數據', 1659968162926); INSERT INTO `handler_test` VALUES (4, '測試插入數據', 1659972053771); INSERT INTO `handler_test` VALUES (5, '測試插入數據', 1659972815670); SET FOREIGN_KEY_CHECKS = 1;
3.2、相關代碼
我隻貼出瞭相關的代碼,其餘代碼在源碼倉庫中有,別慌,傢人們
service
public interface IHandlerTestService extends IService<HandlerTest> { }
TypeHandler 實現類
/** * @author Ning zaichun */ @Slf4j @MappedJdbcTypes({JdbcType.BIGINT}) //對應數據庫類型 @MappedTypes({Date.class}) //java數據類型 public class MyDateTypeHandler implements TypeHandler<Date>{ /** * 入庫前的類型轉換 */ @Override public void setParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException { log.info("setParameter(PreparedStatement ps, int i, Date parameter,JdbcType jdbcType)...."); log.info("[{}],[{}]",parameter,jdbcType); ps.setLong(i, parameter.getTime()); } /** * 查詢後的數據處理 */ @Override public Date getResult(ResultSet rs, String columnName) throws SQLException { log.info("getResult(ResultSet rs, String columnName)...."); log.info("[{}]",columnName); return new Date(rs.getLong(columnName)); } @Override public Date getResult(ResultSet rs, int columnIndex) throws SQLException { log.info("getResult(ResultSet rs, int columnIndex)...."); return new Date(rs.getLong(columnIndex)); } @Override public Date getResult(CallableStatement cs, int columnIndex) throws SQLException { log.info("getResult(CallableStatement cs, int columnIndex)...."); return cs.getDate(columnIndex); } }
實體類的修改,有兩點,
第一點,需要在實體類上加上
@TableName(value = "handler_test",autoResultMap = true)
value 是對應表名,autoResultMap 說的
是否自動構建 resultMap 並使用,
隻生效與 mp 自動註入的 method,
如果設置 resultMap 則不會進行 resultMap 的自動構建並註入,
隻適合個別字段 設置瞭 typeHandler 或 jdbcType 的情況
第二點就是要在需要處理的字段上加上
@TableField(typeHandler = MyDateTypeHandler.class)
註解,class就寫我們自己編寫 Handler.class即可
@Data @TableName(value = "handler_test",autoResultMap = true) @EqualsAndHashCode(callSuper = false) public class HandlerTest implements Serializable { private static final long serialVersionUID = 1L; private String name; /** * 存時間戳 */ @TableField(typeHandler = MyDateTypeHandler.class) @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date date; }
弄完上述這兩點,我們還有一個問題,我之前提到一個註冊,雖然我們指定瞭,也寫好瞭,但實際上,還並沒有註冊到一個存儲 TypeHandler 一個 Map 集合中去的,也就是說Mybatis 在遇到的時候,其實還是不知道它的存在的~。
但其實隻需要在配置文件中加一行即可,原諒我這麼繞圈子,隻是希望說明白這是一步步得來的
type-handlers-package
後面填寫的是我們Handler 存放的包路徑。
有這一步即可。
3.3、測試
@RunWith(SpringRunner.class) @SpringBootTest @ContextConfiguration(classes = HandlerApplication.class) public class HandlerServiceTest { @Autowired IHandlerTestService handlerTestService; @Test public void test1(){ List<HandlerTest> list = handlerTestService.list(); list.forEach(System.out::println); } @Test public void test2(){ HandlerTest handlerTest = new HandlerTest(); handlerTest.setDate(new Date()); handlerTest.setName("測試插入數據"); handlerTestService.save(handlerTest); } }
測試插入
==> Preparing: SELECT name,date FROM handler_test ==> Parameters: <== Columns: name, date <== Row: 測試數據1, 1659967236 2022-08-08 23:55:25.854 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName).... 1659967236 <== Row: 測試數據2, 1659967236 2022-08-08 23:55:25.855 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName).... 1659967236 <== Row: 測試插入數據, 1659968162926 2022-08-08 23:55:25.855 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName).... 1659968162926 <== Row: 測試插入數據, 1659972053771 2022-08-08 23:55:25.855 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName).... 1659972053771 <== Row: 測試插入數據, 1659972815670 2022-08-08 23:55:25.855 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName).... 1659972815670 <== Row: 測試插入數據, 1659974106847 2022-08-08 23:55:25.855 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName).... 1659974106847 <== Row: 測試插入數據, 1659974125542 2022-08-08 23:55:25.855 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName).... 1659974125542 <== Total: 7 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@145113f] HandlerTest(name=測試數據1, date=Tue Jan 20 13:06:07 CST 1970) HandlerTest(name=測試數據2, date=Tue Jan 20 13:06:07 CST 1970) HandlerTest(name=測試插入數據, date=Mon Aug 08 22:16:02 CST 2022) HandlerTest(name=測試插入數據, date=Mon Aug 08 23:20:53 CST 2022) HandlerTest(name=測試插入數據, date=Mon Aug 08 23:33:35 CST 2022) HandlerTest(name=測試插入數據, date=Mon Aug 08 23:55:06 CST 2022) HandlerTest(name=測試插入數據, date=Mon Aug 08 23:55:25 CST 2022) 2022-08-08 23:55:25.863 INFO 7368 --- [ionShutdownHook] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} closing ... 2022-08-08 23:55:25.869 INFO 7368 --- [ionShutdownHook] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} closed
以上就是MyBatisPlus TypeHandler自定義字段類型轉換Handler的詳細內容,更多關於MyBatisPlus字段類型轉換的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Mybatis的類型轉換接口TypeHandler
- 解決mybatis plus字段為null或空字符串無法保存到數據庫的問題
- Java中的BaseTypeHandler自定義類型轉換器的使用
- Mybatis的TypeHandler加解密數據實現
- MybatisPlus如何自定義TypeHandler映射JSON類型為List