I'm trying to retrieve a timestamp date from an oracle database but the code is throwing:
java.lang.IllegalArgumentException: Projection type must be an interface!
I'm trying to use native query because the original query is way to complex to use Spring JPA methods or JPQL.
My code is similar to this one below (Sorry, can't paste the original one due to company policy).
Entity:
@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;
}
Projection:
public interface UserProjection {
String getUserName();
ZonedDateTime getCreatedDate();
}
Repository:
@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);
}
I'm using Spring Boot 2.1.3 and Hibernate 5.3.7.
I had this same issue with a very similar projection:
public interface RunSummary {
String getName();
ZonedDateTime getDate();
Long getVolume();
}
I do not know why, but the issue is with ZonedDateTime
. I switched the type of getDate()
to java.util.Date
, and the exception went away. Outside of the transaction, I transformed the Date back to ZonedDateTime and my downstream code was not affected.
I have no idea why this is an issue; if I don't use projection, the ZonedDateTime works out-of-the-box. I'm posting this as an answer in the meantime because it should be able to serve as a workaround.
According to this bug on the Spring-Data-Commons project, this was a regression caused by adding support for optional fields in the projection. (Clearly it's not actually caused by that other fix -- since that other fix was added in 2020 and this question/answer long predates it.) Regardless, it has been marked as resolved in Spring-Boot 2.4.3.
Basically, you couldn't use any of the Java 8 time classes in your projection, only the older Date-based classes. The workaround I posted above will get around the issue in Spring-Boot versions before 2.4.3.
When you call method from projection interface spring takes the value it received from the database and converts it to the type that method returns. This is done with the 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);
}
In the case of getCreatedDate
method you want to get java.time.ZonedDateTime
from java.sql.Timestamp
. And since ZonedDateTime
is not a collection or an array (if1), not a map (if2) and spring does not have a registered converter (if3) from Timestamp
to ZonedDateTime
, it assumes that this field is another nested projection (else), then this is not the case and you get an exception.
There are two solutions:
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());
}
}
Since version 2.4.0 spring creates a new DefaultConversionService
object instead of getting it via getSharedInstance
and I don't know proper way to get it other than using reflection:
@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());
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With