提问者:小点点

Spring JPA-"java. lang.IllegalArgumentException:投影类型必须是接口!"(使用本机查询)


我正在尝试从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。


共3个答案

匿名用户

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

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没有从TimestampZonedDateTime的注册转换器(if3),它假设此字段是另一个嵌套投影(else),因此情况并非如此,您会得到一个异常。

有两种解决方案:

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

由于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();