Spring JPA - “java.lang.IllegalArgumentException: Projection type 必须是一个接口!”(使用本机查询)

我试图从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。


答案 1

我在一个非常相似的投影中遇到了同样的问题:

public interface RunSummary {

    String getName();
    ZonedDateTime getDate();
    Long getVolume();

}

我不知道为什么,但问题是.我切换了 的类型,例外消失了。在事务之外,我将日期转换回ZonedDateTime,我的下游代码不受影响。ZonedDateTimegetDate()java.util.Date

我不知道为什么这是一个问题;如果我不使用投影,ZonedDateTime开箱即用。同时,我将此作为答案发布,因为它应该能够作为解决方法。


根据Spring-Data-Commons项目上的这个错误,这是由于在投影中添加对可选字段的支持而导致的回归。(显然,它实际上并不是由其他修复程序引起的 - 因为该其他修复程序是在2020年添加的,并且此问题/答案早于它。无论如何,它已在Spring-Boot 2.4.3中标记为已解决。

基本上,您无法在投影中使用任何 Java 8 时间类,只能使用较旧的基于日期的类。我上面发布的解决方法将解决2.4.3之前的Spring-Boot版本中的问题。


答案 2

当您从投影接口调用方法时,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);
}

对于要从 中获取的方法。由于 不是集合或数组 (if1),也不是映射 (if2),并且 spring 没有从 到 的注册转换器 (if3),它假设此字段是另一个嵌套投影(else),那么情况并非如此,您会得到一个例外。getCreatedDatejava.time.ZonedDateTimejava.sql.TimestampZonedDateTimeTimestampZonedDateTime

有两种解决方案:

  1. 返回时间戳,然后手动转换为 ZonedDateTime
  2. 创建和注册转换器
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());
    }
}

Spring Boot 2.4.0 更新:

由于2.4.0版本spring创建了一个新对象,而不是通过它,除了使用反射之外,我不知道正确的方法来获取它:DefaultConversionServicegetSharedInstance

@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());
    }
}

推荐