使用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.lang.Class<?>>, java.util.List<java.lang.Object>, 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); }
这里有几种情况。
- 有对应typeHandler处理当前类型,由typeHandler根据resultSet创建
- 有constructorMappings(构造映射)我们没有设置
- 有默认无参构造,创建一个空对象,使用resultMap进行映射
- 无默认构造,那么根据构造函数,自动映射(不使用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) );
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。