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

Posted

技术标签:

【中文标题】Spring JPA - “java.lang.IllegalArgumentException:投影类型必须是接口!” (使用本机查询)【英文标题】:Spring JPA - "java.lang.IllegalArgumentException: Projection type must be an interface!" (using native query) 【发布时间】:2019-08-10 08:32:14 【问题描述】:

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

【问题讨论】:

"java.lang.IllegalArgumentException: Projection type must be an interface" Error的可能重复 我已经检查了您推荐的帖子,但在那篇文章中,他使用 Spring JPA 方法遇到了问题。如果我的代码可以正常使用(也可以使用 JPQL)。只有当我使用本机查询时它才会失败。就像以前 Spring Data JPA 不支持 Java 8 日期,我们必须手动创建转换器一样。 我也有这个问题。如果我从投影中删除 ZonedDateTime 它就起作用了。不过,我还没有弄清楚如何让它与日期/时间字段一起使用。 @RoddyoftheFrozenPeas 当查询返回的值与投影中使用的 Java 类型不匹配时,我遇到了这个问题。 【参考方案1】:

spring data jpa 无法将数据库中的某些类型转换为java类型的问题。 当我尝试获取布尔值作为结果并且数据库返回数字时,我遇到了几乎相同的问题。

查看更多: https://github.com/spring-projects/spring-data-commons/issues/2223 https://github.com/spring-projects/spring-data-commons/issues/2290

【讨论】:

【参考方案2】:

我在使用 Spring Boot v2.4.2 时遇到了同样的问题 我写了这个为我修复它的丑陋黑客:

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.convert.Jsr310Converters;
import org.springframework.data.util.NullableWrapperConverters;

@Configuration
public class JpaConvertersConfig 

    @EventListener(ApplicationReadyEvent.class)
    public void config() throws Exception 
        Class<?> aClass = Class.forName("org.springframework.data.projection.ProxyProjectionFactory");
        Field field = aClass.getDeclaredField("CONVERSION_SERVICE");
        field.setAccessible(true);
        Field modifiers = Field.class.getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        DefaultConversionService sharedInstance = ((DefaultConversionService) DefaultConversionService.getSharedInstance());
        field.set(null, sharedInstance);
        Jsr310Converters.getConvertersToRegister().forEach(sharedInstance::addConverter);
        NullableWrapperConverters.registerConvertersIn(sharedInstance);
    

【讨论】:

【参考方案3】:

我遇到了同样的问题,投影非常相似:

public interface RunSummary 

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


我不知道为什么,但问题在于ZonedDateTime。我将getDate()的类型切换为java.util.Date,异常就消失了。在事务之外,我将 Date 转换回 ZonedDateTime 并且我的下游代码没有受到影响。

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


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

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

【讨论】:

我在投影中遇到了与OffsetDateTime 相同的问题。我可以使用Instant 解决它,可能比java.util.Date 方便一点。 已报告一个可重现错误的问题,请点赞github.com/spring-projects/spring-data-commons/issues/2260 更新:问题已修复并将包含在 spring boot 2.4.3 中:github.com/spring-projects/spring-data-commons/issues/2223 @singe3 - 我在答案中添加了一条注释,但我不相信。您链接的问题表明是由 2020 年添加的代码引起的。这个问题/答案来自 2019 年。要么它很久以前就坏了,另一个变化是错误地受到指责,要么有两个问题具有相同的症状。 你说得对,这是一个不同的问题。所以也许它被修复了一次然后再次发生然后又被修复了,谁知道【参考方案4】:

当您从投影接口调用方法时,spring 会从数据库中接收到它的值并将其转换为方法返回的类型。这是通过following code 完成的:

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),因此它假定该字段是另一个嵌套的投影(否则),那么情况并非如此,您会遇到异常。

有两种解决方案:

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

【讨论】:

感谢尼克的回答!该解决方案似乎适用于 Spring Boot 2.1。然而,对于 Springboot 2.4,它没有。我对ProjectingMethodInterceptor 进行了更深入的研究,它似乎是由ProxyProjectionFactory 创建的,并且在那里定义了CONVERSION_SERVICE 的新实例,不能再用您的建议进行修改。这似乎是一个已知的错误,在这里报告:jira.spring.io/browse/DATACMNS-1836 github.com/spring-projects/spring-data-commons/issues/2223【参考方案5】:

我从来没有让interface 工作,也不确定ZonedDateTime 是否支持它,尽管我没有不支持的理由。

为此,我创建了一个与投影一起使用的类(当然这可以实现该接口,但为了简单起见,我将其省略了)。

@Getter
@Setter
@AllArgsConstructor
public class UserProjection 
    private String userName;
    private ZonedDateTime createdDate;

这需要 JPQL,因为在查询中使用 NEW 运算符,所以喜欢:

@Query(value = " SELECT NEW org.example.UserProjection(U.userName, U.createdDate) "
        + " FROM USER U " // note that the entity name is "USER" in CAPS
        + " WHERE U.userName = :name ")

【讨论】:

【参考方案6】:

可以创建一个新的属性转换器来将列类型映射到所需的属性类型。

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

【讨论】:

【参考方案7】:

您已在实体中将userId 字段声明为Long,但在UserProjection getUserId 方法中返回类型为String。这是不匹配的,所以改变

String getUserId();

Long getUserId();

【讨论】:

抱歉,创建模拟代码时打错了。我会更新主题。谢谢

以上是关于Spring JPA - “java.lang.IllegalArgumentException:投影类型必须是接口!” (使用本机查询)的主要内容,如果未能解决你的问题,请参考以下文章

spring.jpa.hibernate.hbm2ddl 和 spring.jpa.hibernate.ddl 之间的区别

集成Spring Data JPA

spring data jpa怎么和solr整合

spring data jpa问题

Spring Data JPA 整合Spring

[Spring Boot] Adding JPA and Spring Data JPA