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 没有从Timestamp
到ZonedDateTime
的注册转换器(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 之间的区别