使用lombok注解导致mybatis-plus TypeHandler失效

问题描述

建立实体其中一个字段为枚举类

/**
 * @author liuxishan 2023/6/2
 */
@Data
@Builder
@TableName(value = "hot_event",autoResultMap = false)
@EqualsAndHashCode(callSuper = true)
public class HotEvent extends BaseAuditor {
    @TableId(type = IdType.ASSIGN_ID)
    private String id;
   ············
   ············ 
    /**
     * 创建方式
     */
    @TableField(typeHandler = HotEventCreationMethodHandler.class,jdbcType = JdbcType.INTEGER)
    private CreationMethodEnum creationMethod;

}

public enum CreationMethodEnum {
    SYSTEM(0, "系统导入"),
    MANUAL_CREATE(1, "手动导入");

    /**
     * 用于排序
     */
    private Integer value;

    private String msg;

    CreationMethodEnum(Integer value, String msg) {
        this.value = value;
        this.msg = msg;
    }

    public Integer getValue() {
        return value;
    }

    public String getMsg() {
        return msg;
    }

    public static CreationMethodEnum valueOf(int value) {
        for (CreationMethodEnum creationMethodEnum : CreationMethodEnum.values()) {
            if (creationMethodEnum.value.equals(value)) {
                return creationMethodEnum;
            }
        }
        return null;
    }
}

希望数据库存的时对应的数字
为了和数据库进行转换,使用了typeHandler

public class HotEventCreationMethodHandler implements TypeHandler<CreationMethodEnum> {

    @Override
    public void setParameter(PreparedStatement preparedStatement, int i, CreationMethodEnum creationMethodEnum, JdbcType jdbcType) throws SQLException {
        preparedStatement.setInt(i,creationMethodEnum.getValue());
    }

    @Override
    public CreationMethodEnum getResult(ResultSet resultSet, String columnName) throws SQLException {
        int anInt = resultSet.getInt(columnName);
        return CreationMethodEnum.valueOf(anInt);
    }

    @Override
    public CreationMethodEnum getResult(ResultSet resultSet, int i) throws SQLException {
        int anInt = resultSet.getInt(i);
        return CreationMethodEnum.valueOf(anInt);
    }

    @Override
    public CreationMethodEnum getResult(CallableStatement callableStatement, int i) throws SQLException {
        int anInt = callableStatement.getInt(i);
        return CreationMethodEnum.valueOf(anInt);
    }
}

测试发现

 @Test
    public void pageContents() {
      HotEvent hotEvent = HotEvent.builder()
                .eventName("测试事件名称")
                .eventOverview("测试测试")
                .eventType(EventTypeEnum.FUND)
                .bizId("1245454")
                .creationMethod(CreationMethodEnum.MANUAL_CREATE)
                .publishingTime(LocalDateTime.now())
                .url("http://baidu.com")
                .publishingAgency("机构")
                .build();
        hotEventDao.insert(hotEvent);
        List<HotEvent> hotEvents = hotEventDao.selectList(Wrappers.<HotEvent>lambdaQuery().eq(HotEvent::getCreationMethod, 1));
     
    }

插入/更新typeHandler生效,但是查询时在将数据库数据映射成java实体类的时候报错

Caused by: java.lang.IllegalArgumentException: No enum constant com.yiyouliao.rivers.content.api.enums.CreationMethodEnum.1
	at java.lang.Enum.valueOf(Enum.java:238)
	at org.apache.ibatis.type.EnumTypeHandler.getNullableResult(EnumTypeHandler.java:49)
	at org.apache.ibatis.type.EnumTypeHandler.getNullableResult(EnumTypeHandler.java:26)
	at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:85)
	... 67 more

设置的HotEventCreationMethodHandler并没有生效
我们知道,在不开启autoResultMap时,会导致TableField对于查询返回的结果不生效。(对于更新无影响)
因此首先检查在实体类注解上已经开启了autoResultMap,仍然是报错(mybatisPlus会根据我们的实体类的类型,为我们自动注入resultMap,注入的resultMap也会自动推断出需要使用的类型转换器当然我们也可以指定)

@TableName(value = "hot_event",autoResultMap = true)

问题排查

网上查了typeHandler失效的原因,大部分都说的是检查autoResultMap。无奈只能跟源码进行检查
问题出在查询结果后将结果转换为java实体类的地方,由DefaultResultSetHandler进行处理
关键方法org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSet

org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getRowValue(org.apache.ibatis.executor.resultset.ResultSetWrapper, org.apache.ibatis.mapping.ResultMap, java.lang.String)
在处理一行数据是,正常情况是首先创建java实体对象,然后根据resultMap的字段映射进行转换

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
   //创建java实体对象
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final MetaObject metaObject = configuration.newMetaObject(rowValue);
        boolean foundValues = this.useConstructorMappings;
        if (shouldApplyAutomaticMappings(resultMap, false)) {
            foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
        }
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
        foundValues = lazyLoader.size() > 0 || foundValues;
        rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
}

但是发现我们在createResultObject这一步就报错了,

Caused by: java.lang.IllegalArgumentException: No enum constant com.yiyouliao.rivers.content.api.enums.CreationMethodEnum.1

跟进去原因就找到了,在创建对象时
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#createResultObject(org.apache.ibatis.executor.resultset.ResultSetWrapper, org.apache.ibatis.mapping.ResultMap, java.util.List>, java.util.List, java.lang.String)

 private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)
      throws SQLException {
    final Class<?> resultType = resultMap.getType();
    final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
    final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
    if (hasTypeHandlerForResultObject(rsw, resultType)) {
      return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
    } else if (!constructorMappings.isEmpty()) {
      return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
    } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
      return objectFactory.create(resultType);
    } else if (shouldApplyAutomaticMappings(resultMap, false)) {
      return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs);
    }
    throw new ExecutorException("Do not know how to create an instance of " + resultType);
  }

这里有几种情况。

  1. 有对应typeHandler处理当前类型,由typeHandler根据resultSet创建
  2. 有constructorMappings(构造映射)我们没有设置
  3. 有默认无参构造,创建一个空对象,使用resultMap进行映射
  4. 无默认构造,那么根据构造函数,自动映射(不使用resultMap)进行映射

发现进了第四个处理逻辑导致报错,那么问题就发现了,是因为我们的实体类没有默认的无参构造函数

问题结论

回头检查实体类发现我们加了@Builder注解,会为实体类创建构造函数,但是也导致实体类没有了无参构造。
因此加上注解NoArgsConstructor,问题解决

/**
 * @author liuxishan 2023/6/2
 */
@Data
@Builder
@TableName(value = "hot_event",autoResultMap = false)
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
public class HotEvent extends BaseAuditor {
    @TableId(type = IdType.ASSIGN_ID)
    private String id;
   ············
   ············ 
    /**
     * 创建方式
     */
    @TableField(typeHandler = HotEventCreationMethodHandler.class,jdbcType = JdbcType.INTEGER)
    private CreationMethodEnum creationMethod;

}

Tips

在查询时使用Wrappers构造查询条件时,查询字段是枚举时,不能直接使用枚举,需要使用数据库对应的值进行查询,因为typeHandler仅仅处理的是实体类字段和jdbcType的转换
错误写法

   List<HotEvent> hotEvents = hotEventDao.selectList(
       Wrappers.<HotEvent>lambdaQuery().eq(HotEvent::getCreationMethod, CreationMethodEnum.MANUAL_CREATE)
   );

正确写法

   List<HotEvent> hotEvents = hotEventDao.selectList(
       Wrappers.<HotEvent>lambdaQuery().eq(HotEvent::getCreationMethod,1)
   );

你可能感兴趣的:(mybatis,mybatis,java,开发语言)