【问题标题】:Spring JPA - "java.lang.IllegalArgumentException: Projection type must be an interface!" (using native query)Spring JPA - “java.lang.IllegalArgumentException:投影类型必须是接口!” (使用本机查询)
【发布时间】: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。

【问题讨论】:

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

标签: java spring hibernate spring-boot spring-data-jpa


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

      有两种解决方案:

      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 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
      【解决方案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();

          【讨论】:

          • 抱歉,创建模拟代码时打错了。我会更新主题。谢谢
          猜你喜欢
          • 2018-03-31
          • 1970-01-01
          • 2018-12-16
          • 1970-01-01
          • 2019-02-26
          • 1970-01-01
          • 1970-01-01
          • 2021-01-08
          • 2020-01-10
          相关资源
          最近更新 更多