• MyBatisPlus中的TypeHandler


    前言

    今天遇上这样的一个情况,

    在 MySQL 中,字段的属性为 BigInt ,按道理来说,对应 Java 中的 Long 类型。

    但实际上项目中与之对应的 Java对象中的属性的类型是 Date 类型, 直接给我这个废物当头一棒

    而且不是一两张表,是比较多的表处于 Date 和 BigInt 混用的情况,

    你说要好好用Date就好好用Date,要好好用时间戳就好好用时间戳啊,还混用,类型还不对应,麻了

    (别问这个项目怎么出现这种事情的,就是来了人,又走了人,然后填坑)

    保持微笑:grinning:(此处口吐芬芳xxxxxx)

    一、思考

    我想知道出现这种情况,你是如何思考的?

    我的思考是,到底是改数据库,还是改程序代码比较好。

    但是无论哪一种我都不敢轻举妄动,所以我做的第一步是把数据库和代码备份,确保不会被玩坏。

    我也问了同事,他的建议是让我改程序。

    但是怎么说勒,我细细比较了改代码和改程序的麻烦程度,改数据表麻烦会少很多,我就在表结构中的Bigint 类型改为 datatime 类型,而且当时我的任务,是只局限于一两张业务表,影响范围不大,引用也不多。

    我就兴冲冲的把表结构改了,然后把任务完成了~

    等到今天上午,我之前询问的那个同事也遇到这个问题,他就向上面的经理提了一嘴,说时间类型不对,问他标准是哪一种,经理说是时间戳,我心里一凉~,麻了,(此处省略一万句)

    听完,我就去把表结构改回来了,此时备份就发生作用了~,还原完数据表后,我就打算去改程序代码了

    周一写 bug,bug 改一周

    突然他和我聊到,xxx,你知道MybatisPlus,有什么转换的方法吗?

    这每一个都要改,太麻烦了,而且业务代码中肯定也用到了,这改起来代价太大了,有没有注解的方式可以解决转换问题。

    很浅显的思考,我能够感觉到自己的经验的不足,对于很多偷懒(思考),我还是差的太远了。

    二、解决方式

    因为用到的 ORM 框架是 MybatisPlus,所以首先找的就是有没有官方的支持。

    继而就在官网找到一个字段类型处理器,一看才发现,是学过的东西啊,只怪用的太少,知道的太少啊。

    然后根据这个线索继续找,就了解到 MyBatis-Plus 字段类型处理器 TypeHandler

    就翻看源码,想用一个东西,最快的方式就是看一下源码的实现

    2.1、TypeHandler源码

    1. public interface TypeHandler {
    2. /**
    3. * 入库前的类型转换
    4. */
    5. void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
    6. /**
    7. * 得到结果。
    8. * 查询后的数据处理
    9. */
    10. T getResult(ResultSet rs, String columnName) throws SQLException;
    11. T getResult(ResultSet rs, int columnIndex) throws SQLException;
    12. T getResult(CallableStatement cs, int columnIndex) throws SQLException;
    13. }

    找到接口,看一下源码中针对已有属性是如何处理,我们仿写一份,达到我们的要求即可啊.

    2.2、BaseTypeHandler 源码

    有这么多,我们直接看一下 BaseTypeHandler 是什么样的处理逻辑,

    一方面 base 吗,基础吗,我们就看看基础是什么样的处理啦,另外一方面他是抽象类吗,说明它其他实现类的基类吗。

    1. public abstract class BaseTypeHandler extends TypeReference implements TypeHandler {
    2. @Override
    3. public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    4. if (parameter == null) {
    5. if (jdbcType == null) {
    6. throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
    7. }
    8. try {
    9. ps.setNull(i, jdbcType.TYPE_CODE);
    10. } catch (SQLException e) {
    11. throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
    12. + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
    13. + "Cause: " + e, e);
    14. }
    15. } else {
    16. try {
    17. setNonNullParameter(ps, i, parameter, jdbcType);
    18. } catch (Exception e) {
    19. throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
    20. + "Try setting a different JdbcType for this parameter or a different configuration property. "
    21. + "Cause: " + e, e);
    22. }
    23. }
    24. }
    25. @Override
    26. public T getResult(ResultSet rs, String columnName) throws SQLException {
    27. try {
    28. return getNullableResult(rs, columnName);
    29. } catch (Exception e) {
    30. throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set. Cause: " + e, e);
    31. }
    32. }
    33. @Override
    34. public T getResult(ResultSet rs, int columnIndex) throws SQLException {
    35. try {
    36. return getNullableResult(rs, columnIndex);
    37. } catch (Exception e) {
    38. throw new ResultMapException("Error attempting to get column #" + columnIndex + " from result set. Cause: " + e, e);
    39. }
    40. }
    41. @Override
    42. public T getResult(CallableStatement cs, int columnIndex) throws SQLException {
    43. try {
    44. return getNullableResult(cs, columnIndex);
    45. } catch (Exception e) {
    46. throw new ResultMapException("Error attempting to get column #" + columnIndex + " from callable statement. Cause: " + e, e);
    47. }
    48. }
    49. // 这里就是设置为 不为 null 时的入库
    50. public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
    51. /**
    52. * 获取可为空的结果。
    53. */
    54. public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;
    55. public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;
    56. public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;
    57. }

    看起来好像很长很多的样子:当我们去掉那些判断,精简一下:

    1. public abstract class BaseTypeHandler extends TypeReference implements TypeHandler {
    2. @Override
    3. public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    4. // 设置不为null的参数,进行入库 ,此处是抽象类,下层还有实现类,
    5. // 记住这里,待会带你看实现类,你就知道了
    6. setNonNullParameter(ps, i, parameter, jdbcType);
    7. }
    8. @Override
    9. public T getResult(ResultSet rs, String columnName) throws SQLException {
    10. // 这里从数据库中获取到数据,然后进行类型的一个设置
    11. return getNullableResult(rs, columnName);
    12. }
    13. @Override
    14. public T getResult(ResultSet rs, int columnIndex) throws SQLException {
    15. //这两个抽象方法,给我的感觉是一模一样的,包括下一个也是如此
    16. return getNullableResult(rs, columnIndex);
    17. }
    18. @Override
    19. public T getResult(CallableStatement cs, int columnIndex) throws SQLException {
    20. return getNullableResult(cs, columnIndex);
    21. }
    22. }

    2.3、BigIntegerTypeHandler 源码中的实现类

    1. public class BigIntegerTypeHandler extends BaseTypeHandler {
    2. @Override
    3. public void setNonNullParameter(PreparedStatement ps, int i, BigInteger parameter, JdbcType jdbcType) throws SQLException {
    4. // 这里是转为 BigDecimal ,所以这里就算 setBigDecimal,
    5. // 那么我们就可以猜测,它还支持其他的方法,Date的话,那就是setDate
    6. ps.setBigDecimal(i, new BigDecimal(parameter));
    7. }
    8. @Override
    9. public BigInteger getNullableResult(ResultSet rs, String columnName) throws SQLException {
    10. BigDecimal bigDecimal = rs.getBigDecimal(columnName);
    11. // 这里是rs.getBigDecimal ,我们待会去试一下能否getDate就可以了
    12. return bigDecimal == null ? null : bigDecimal.toBigInteger();
    13. }
    14. // 这两个暂时没有做了解,Debug的时候,断点没有执行到这,后期再补一块的知识
    15. // 但是为了以防万一,我们待会也会照着它的方式将代码改成这样
    16. @Override
    17. public BigInteger getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    18. BigDecimal bigDecimal = rs.getBigDecimal(columnIndex);
    19. return bigDecimal == null ? null : bigDecimal.toBigInteger();
    20. }
    21. @Override
    22. public BigInteger getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    23. BigDecimal bigDecimal = cs.getBigDecimal(columnIndex);
    24. return bigDecimal == null ? null : bigDecimal.toBigInteger();
    25. }
    26. }

    这个实现类,没什么代码,而且就是set、get ,并没有其他的一些处理逻辑什么的。

    那么我们也照这样的方式实现一个。

    2.4、尝试

    先明确目标,我们Mysql 中的字段类型 为 BigInt ,Java程序中的属性类型为 Date ,

    所以我们在入库的时候就是要将 Date 类型转化为 Long 进行入库,

    在从数据库中取出来的时候,要从 Long 类型转化为 Date 映射到 JavaBean中

    我们直接copy上面的代码,然后进行一些更改

    1. public class MyDateTypeHandler implements TypeHandler{
    2. /**
    3. * 入库前的类型转换 即执行insert、update方法时会执行
    4. */
    5. @Override
    6. public void setParameter(PreparedStatement ps, int i, Date parameter,
    7. JdbcType jdbcType) throws SQLException {
    8. log.info("setParameter(PreparedStatement ps, int i, Date parameter,JdbcType jdbcType)....");
    9. log.info("[{}],[{}]",parameter,jdbcType);
    10. ps.setLong(i, parameter.getTime());
    11. }
    12. /**
    13. * 查询后的数据处理
    14. * 这就是查询出来,进行映射的时候,会执行这段代码
    15. */
    16. @Override
    17. public Date getResult(ResultSet rs, String columnName) throws SQLException {
    18. log.info("getResult(ResultSet rs, String columnName)....",columnName);
    19. return new Date(rs.getLong(columnName));
    20. }
    21. @Override
    22. public Date getResult(ResultSet rs, int columnIndex) throws SQLException {
    23. log.info("getResult(ResultSet rs, int columnIndex)....");
    24. return new Date(rs.getLong(columnIndex));
    25. }
    26. @Override
    27. public Date getResult(CallableStatement cs, int columnIndex)
    28. throws SQLException {
    29. log.info("getResult(CallableStatement cs, int columnIndex)....");
    30. return cs.getDate(columnIndex);
    31. }
    32. }

    咋一眼好像成功啦,但是我们忽略了一个问题,那些默认的允许进行相互进行类型转换的类,它在程序启动的时候,就会被注册进去了。

    而且我们写了这个类,还没有指明给谁使用,怎么使用?

    基于此,我写了一个小Demo,希望大家能够弄明白,以后遇上也能够解决一些问题

    三、实践案例

    3.1、数据库

    数据库

    1. SET NAMES utf8mb4;
    2. SET FOREIGN_KEY_CHECKS = 0;
    3. -- ----------------------------
    4. -- Table structure for handler_test
    5. -- ----------------------------
    6. DROP TABLE IF EXISTS `handler_test`;
    7. CREATE TABLE `handler_test` (
    8. `id` int(11) NOT NULL AUTO_INCREMENT,
    9. `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    10. `date` bigint(50) NOT NULL COMMENT '存时间戳',
    11. PRIMARY KEY (`id`) USING BTREE
    12. ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    13. -- ----------------------------
    14. -- Records of handler_test
    15. -- ----------------------------
    16. INSERT INTO `handler_test` VALUES (1, '测试数据1', 1659967236);
    17. INSERT INTO `handler_test` VALUES (2, '测试数据2', 1659967236);
    18. INSERT INTO `handler_test` VALUES (3, '测试插入数据', 1659968162926);
    19. INSERT INTO `handler_test` VALUES (4, '测试插入数据', 1659972053771);
    20. INSERT INTO `handler_test` VALUES (5, '测试插入数据', 1659972815670);
    21. SET FOREIGN_KEY_CHECKS = 1;

    3.2、相关代码

    我只贴出了相关的代码,其余代码在源码仓库中有,别慌,家人们

    service

    1. public interface IHandlerTestService extends IService {
    2. }

    TypeHandler 实现类

    1. /**
    2. * @author Ning zaichun
    3. */
    4. @Slf4j
    5. @MappedJdbcTypes({JdbcType.BIGINT}) //对应数据库类型
    6. @MappedTypes({Date.class}) //java数据类型
    7. public class MyDateTypeHandler implements TypeHandler{
    8. /**
    9. * 入库前的类型转换
    10. */
    11. @Override
    12. public void setParameter(PreparedStatement ps, int i, Date parameter,
    13. JdbcType jdbcType) throws SQLException {
    14. log.info("setParameter(PreparedStatement ps, int i, Date parameter,JdbcType jdbcType)....");
    15. log.info("[{}],[{}]",parameter,jdbcType);
    16. ps.setLong(i, parameter.getTime());
    17. }
    18. /**
    19. * 查询后的数据处理
    20. */
    21. @Override
    22. public Date getResult(ResultSet rs, String columnName) throws SQLException {
    23. log.info("getResult(ResultSet rs, String columnName)....");
    24. log.info("[{}]",columnName);
    25. return new Date(rs.getLong(columnName));
    26. }
    27. @Override
    28. public Date getResult(ResultSet rs, int columnIndex) throws SQLException {
    29. log.info("getResult(ResultSet rs, int columnIndex)....");
    30. return new Date(rs.getLong(columnIndex));
    31. }
    32. @Override
    33. public Date getResult(CallableStatement cs, int columnIndex)
    34. throws SQLException {
    35. log.info("getResult(CallableStatement cs, int columnIndex)....");
    36. return cs.getDate(columnIndex);
    37. }
    38. }

    实体类的修改,有两点,第一点,需要在实体类上加上

    1. @TableName(value = "handler_test",autoResultMap = true) value 是对应表名,autoResultMap 说的

      1. 是否自动构建 resultMap 并使用,
      2. 只生效与 mp 自动注入的 method,
      3. 如果设置 resultMap 则不会进行 resultMap 的自动构建并注入,
      4. 只适合个别字段 设置了 typeHandler 或 jdbcType 的情况

    2. 第二点就是要在需要处理的字段上加上 @TableField(typeHandler = MyDateTypeHandler.class) 注解,class就写我们自己编写 Handler.class即可

    1. @Data
    2. @TableName(value = "handler_test",autoResultMap = true)
    3. @EqualsAndHashCode(callSuper = false)
    4. public class HandlerTest implements Serializable {
    5. private static final long serialVersionUID = 1L;
    6. private String name;
    7. /**
    8. * 存时间戳
    9. */
    10. @TableField(typeHandler = MyDateTypeHandler.class)
    11. @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    12. @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    13. private Date date;
    14. }

    弄完上述这两点,我们还有一个问题,我之前提到一个注册,虽然我们指定了,也写好了,但实际上,并没有注册到一个存储 TypeHandler 一个 Map 集合中去的,Mybatis 在遇到的时候,仍然是会报错的。

    但其实只需要在配置文件中加一行即可, 原谅我这么绕圈子,只是希望说明白这是一步步得来的

    type-handlers-package 后面填写的是我们Handler 存放的包路径。

    有这一步即可。

    3.3、测试

    1. @RunWith(SpringRunner.class)
    2. @SpringBootTest
    3. @ContextConfiguration(classes = HandlerApplication.class)
    4. public class HandlerServiceTest {
    5. @Autowired
    6. IHandlerTestService handlerTestService;
    7. @Test
    8. public void test1(){
    9. List list = handlerTestService.list();
    10. list.forEach(System.out::println);
    11. }
    12. @Test
    13. public void test2(){
    14. HandlerTest handlerTest = new HandlerTest();
    15. handlerTest.setDate(new Date());
    16. handlerTest.setName("测试插入数据");
    17. handlerTestService.save(handlerTest);
    18. }
    19. }

    测试插入

    1. ==> Preparing: SELECT name,date FROM handler_test
    2. ==> Parameters:
    3. <== Columns: name, date
    4. <== Row: 测试数据1, 1659967236
    5. 2022-08-08 23:55:25.854 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName)....
    6. 1659967236
    7. <== Row: 测试数据2, 1659967236
    8. 2022-08-08 23:55:25.855 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName)....
    9. 1659967236
    10. <== Row: 测试插入数据, 1659968162926
    11. 2022-08-08 23:55:25.855 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName)....
    12. 1659968162926
    13. <== Row: 测试插入数据, 1659972053771
    14. 2022-08-08 23:55:25.855 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName)....
    15. 1659972053771
    16. <== Row: 测试插入数据, 1659972815670
    17. 2022-08-08 23:55:25.855 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName)....
    18. 1659972815670
    19. <== Row: 测试插入数据, 1659974106847
    20. 2022-08-08 23:55:25.855 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName)....
    21. 1659974106847
    22. <== Row: 测试插入数据, 1659974125542
    23. 2022-08-08 23:55:25.855 INFO 7368 --- [ main] com.nzc.demo.handler.MyDateTypeHandler : getResult(ResultSet rs, String columnName)....
    24. 1659974125542
    25. <== Total: 7
    26. Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@145113f]
    27. HandlerTest(name=测试数据1, date=Tue Jan 20 13:06:07 CST 1970)
    28. HandlerTest(name=测试数据2, date=Tue Jan 20 13:06:07 CST 1970)
    29. HandlerTest(name=测试插入数据, date=Mon Aug 08 22:16:02 CST 2022)
    30. HandlerTest(name=测试插入数据, date=Mon Aug 08 23:20:53 CST 2022)
    31. HandlerTest(name=测试插入数据, date=Mon Aug 08 23:33:35 CST 2022)
    32. HandlerTest(name=测试插入数据, date=Mon Aug 08 23:55:06 CST 2022)
    33. HandlerTest(name=测试插入数据, date=Mon Aug 08 23:55:25 CST 2022)
    34. 2022-08-08 23:55:25.863 INFO 7368 --- [ionShutdownHook] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} closing ...
    35. 2022-08-08 23:55:25.869 INFO 7368 --- [ionShutdownHook] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} closed
  • 相关阅读:
    Shell编程实际应用
    STL-stack
    JD(按关键字搜索商品)API接口
    拦截器
    Hadoop学习日志之HDFS文件系统
    RDP方式连接服务器上传文件方法
    内网可以通过https来访问,外网不可以通过https来访问,怎么办
    46道Redis面试题,含参考答案!
    Emlog评论区显示用户操作系统与浏览器信息教程
    node对接微信支付,微信返回失败
  • 原文地址:https://blog.csdn.net/Q54665642ljf/article/details/126250025