Я пытаюсь получить дату метки времени из базы данных оракула, но код выдает:
java.lang.IllegalArgumentException: Projection type must be an interface!
Я пытаюсь использовать собственный запрос, потому что исходный запрос слишком сложен для использования методов 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, используемому в проекции.




Вы объявили поле userId как Long в объекте, но в методе UserProjectiongetUserId тип возвращаемого значения — String. что не соответствует так изменить
String getUserId();
к
Long getUserId();
Извините, опечатка при создании макета кода. обновлю тему. Спасибо
У меня была такая же проблема с очень похожей проекцией:
public interface RunSummary {
String getName();
ZonedDateTime getDate();
Long getVolume();
}
Не знаю почему, но проблема с ZonedDateTime. Я переключил тип getDate() на java.util.Date, и исключение исчезло. Вне транзакции я преобразовал Date обратно в ZonedDateTime, и мой нижестоящий код не пострадал.
Я понятия не имею, почему это проблема; если я не использую проекцию, ZonedDateTime работает «из коробки». Тем временем я публикую это как ответ, потому что он должен служить обходным путем.
Согласно эта ошибка в проекте Spring-Data-Commons, это регресс, вызванный добавлением поддержки необязательных полей в проекцию. (Очевидно, что на самом деле это не вызвано этим другим исправлением - поскольку это другое исправление было добавлено в 2020 году, а этот вопрос / ответ задолго до него.) Несмотря на это, оно было помечено как разрешенное в Spring-Boot 2.4.3.
По сути, вы не могли использовать в своей проекции ни один из классов времени Java 8, только старые классы на основе даты. Обходной путь, который я опубликовал выше, решит проблему в версиях Spring-Boot до 2.4.3.
У меня была такая же проблема с OffsetDateTime в моей проекции. Я смог решить это с помощью Instant, может быть, немного удобнее, чем java.util.Date.
Сообщается о проблеме с воспроизводимой ошибкой, проголосуйте за github.com/spring-projects/spring-data-commons/issues/2260.
Обновление: проблема исправлена и будет включена в весеннюю загрузку 2.4.3: github.com/spring-projects/spring-data-commons/issues/2223
@ singe3 - я добавил примечание к ответу, но не убежден. Указано, что проблемы, на которые вы ссылаетесь, вызваны кодом, добавленным в 2020 году. Этот вопрос / ответ относится к 2019 году. Либо он был сломан задолго до этого, а другое изменение ошибочно обвиняется, либо есть две проблемы с одинаковыми симптомами.
Вы правы, это другой вопрос. Так что, может быть, это было исправлено один раз, затем повторилось, а затем снова было исправлено, кто знает.
Можно создать новый преобразователь атрибутов для сопоставления типа столбца с желаемым типом атрибута.
@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();
Когда вы вызываете метод из интерфейса проекции, 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.time.ZonedDateTime из java.sql.Timestamp. А так как ZonedDateTime не является коллекцией или массивом (if1), не картой (if2) и у Spring нет зарегистрированного преобразователя (if3) из Timestamp в 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());
}
}
Начиная с версии 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());
}
}
Спасибо за ответ Ник! Решение, кажется, работает для Spring Boot 2.1. Однако для Springboot 2.4 это не так. Я копнул глубже в ProjectingMethodInterceptor, кажется, он создан ProxyProjectionFactory, и там определен новый экземпляр CONVERSION_SERVICE, который больше не может быть изменен с помощью того, что вы предложили. Это, похоже, известная ошибка, о ней сообщается здесь: jira.spring.io/browse/DATACMNS-1836.
Я никогда не заставлял 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 ")
У меня была такая же проблема с 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);
}
}
Проблема заключается в том, что jpa данных spring не может преобразовать некоторые типы из базы данных в типы java. У меня была почти такая же проблема, когда я пытался получить логическое значение в качестве результата, а база данных возвращает число.
Смотрите больше на: https://github.com/spring-projects/spring-data-commons/issues/2223https://github.com/spring-projects/spring-data-commons/issues/2290
Возможный дубликат «java.lang.IllegalArgumentException: тип проекции должен быть интерфейсом» Ошибка