我正在尝试从oracle数据库中检索时间戳日期,但代码正在抛出:
java. lang.IllegalArgumentException:投影类型必须是接口!
我正在尝试使用本机查询,因为原始查询使用Spring JPA方法或JPQL非常复杂。
我的代码与下面的代码类似(抱歉,由于公司政策,无法粘贴原始代码)。
实体:
@Getter
@Setter
@Entity(name = "USER")
public class User {
@Column(name = "USER_ID")
private Long userId;
@Column(name = "USER_NAME")
private String userName;
@Column(name = "CREATED_DATE")
private ZonedDateTime createdDate;
}
投影:
public interface UserProjection {
String getUserName();
ZonedDateTime getCreatedDate();
}
存储库:
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
@Query(
value = " select userName as userName," +
" createdDate as createdDate" +
" from user as u " +
" where u.userName = :name",
nativeQuery = true
)
Optional<UserProjection> findUserByName(@Param("name") String name);
}
我正在使用Spring Boot 2.1.3和Hibernate 5.3.7。
我在一个非常相似的预测中遇到了同样的问题:
public interface RunSummary {
String getName();
ZonedDateTime getDate();
Long getVolume();
}
我不知道为什么,但问题在于ZonedDateTime
。我将getDate()
的类型切换为java.util.Date
,异常消失了。在事务之外,我将Date转换回ZonedDateTime,我的下游代码不受影响。
我不知道为什么这是一个问题;如果我不使用投影,ZonedDateTime可以开箱即用。在此期间,我将此作为答案发布,因为它应该可以作为一种解决方法。
根据Spring-Data-Commons项目的bug,这是由于在投影中添加了对可选字段的支持而导致的回归。(很明显,这实际上不是由另一个修复引起的——因为另一个修复是在2020年添加的,这个问题/答案早在它之前。)无论如何,它在Spring-Boot 2.4.3中已被标记为已解决。
基本上,您不能在投影中使用Java8个时间类中的任何一个,只能使用较旧的基于日期的类。我上面发布的解决方法将在2.4.3之前的Spring Boot版本中解决这个问题。
当您从投影接口调用方法时,Spring获取它从数据库接收到的值并将其转换为方法返回的类型。这是通过以下代码完成的:
if (type.isCollectionLike() && !ClassUtils.isPrimitiveArray(rawType)) { //if1
return projectCollectionElements(asCollection(result), type);
} else if (type.isMap()) { //if2
return projectMapValues((Map<?, ?>) result, type);
} else if (conversionRequiredAndPossible(result, rawType)) { //if3
return conversionService.convert(result, rawType);
} else { //else
return getProjection(result, rawType);
}
在getCreatedDate
方法的情况下,您希望从java. sql.Timestamp
获取java.time.ZonedDateTime
。由于ZonedDateTime
不是集合或数组(if1),不是映射(if2),并且Spring没有从Timestamp
到ZonedDateTime
的注册转换器(if3),它假设此字段是另一个嵌套投影(else),因此情况并非如此,您会得到一个异常。
有两种解决方案:
public class TimestampToZonedDateTimeConverter implements Converter<Timestamp, ZonedDateTime> {
@Override
public ZonedDateTime convert(Timestamp timestamp) {
return ZonedDateTime.now(); //write your algorithm
}
}
@Configuration
public class ConverterConfig {
@EventListener(ApplicationReadyEvent.class)
public void config() {
DefaultConversionService conversionService = (DefaultConversionService) DefaultConversionService.getSharedInstance();
conversionService.addConverter(new TimestampToZonedDateTimeConverter());
}
}
由于2.4.0版本Spring创建了一个新的DefaultConversionService
对象,而不是通过getSharedInstance
获取它,除了使用反射之外,我不知道正确的方法来获取它:
@Configuration
public class ConverterConfig implements WebMvcConfigurer {
@PostConstruct
public void config() throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException {
Class<?> aClass = Class.forName("org.springframework.data.projection.ProxyProjectionFactory");
Field field = aClass.getDeclaredField("CONVERSION_SERVICE");
field.setAccessible(true);
GenericConversionService service = (GenericConversionService) field.get(null);
service.addConverter(new TimestampToZonedDateTimeConverter());
}
}
可以创建一个新的属性转换器来将列类型映射到所需的属性类型。
@Component
public class OffsetDateTimeTypeConverter implements
AttributeConverter<OffsetDateTime, Timestamp> {
@Override
public Timestamp convertToDatabaseColumn(OffsetDateTime attribute) {
//your implementation
}
@Override
public OffsetDateTime convertToEntityAttribute(Timestamp dbData) {
return dbData == null ? null : dbData.toInstant().atOffset(ZoneOffset.UTC);
}
}
在投影中,它可以像下面这样使用。这是一种调用转换器的显式方式。我找不到如何自动注册它,这样你就不需要每次需要时都添加@Value
注释。
@Value("#{@offsetDateTimeTypeConverter.convertToEntityAttribute(target.yourattributename)}")
OffsetDateTime getYourAttributeName();