Spring JPA — «java.lang.IllegalArgumentException: тип проекции должен быть интерфейсом!» (используя собственный запрос)

Я пытаюсь получить дату метки времени из базы данных оракула, но код выдает:

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, и нам приходится создавать конвертер вручную.

Fábio Castilhos 19.03.2019 19:11

У меня тоже была эта пробема. Если бы я удалил ZonedDateTime из проекции, это сработало. Однако я не понял, как заставить его работать с полем даты/времени.

Roddy of the Frozen Peas 22.03.2019 20:42

@RoddyoftheFrozenPeas У меня возникла эта проблема, когда значение, возвращаемое из запроса, не соответствовало типу Java, используемому в проекции.

Paul A. Trzyna 30.07.2021 15:51
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
16
4
7 644
7

Ответы 7

Вы объявили поле userId как Long в объекте, но в методе UserProjectiongetUserId тип возвращаемого значения — String. что не соответствует так изменить

String getUserId();

к

Long getUserId();

Извините, опечатка при создании макета кода. обновлю тему. Спасибо

Fábio Castilhos 19.03.2019 19:13

У меня была такая же проблема с очень похожей проекцией:

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.

Moritz 05.11.2020 12:32

Сообщается о проблеме с воспроизводимой ошибкой, проголосуйте за github.com/spring-projects/spring-data-commons/issues/2260.

singe3 20.01.2021 11:27

Обновление: проблема исправлена ​​и будет включена в весеннюю загрузку 2.4.3: github.com/spring-projects/spring-data-commons/issues/2223

singe3 20.01.2021 11:29

@ singe3 - я добавил примечание к ответу, но не убежден. Указано, что проблемы, на которые вы ссылаетесь, вызваны кодом, добавленным в 2020 году. Этот вопрос / ответ относится к 2019 году. Либо он был сломан задолго до этого, а другое изменение ошибочно обвиняется, либо есть две проблемы с одинаковыми симптомами.

Roddy of the Frozen Peas 20.01.2021 14:40

Вы правы, это другой вопрос. Так что, может быть, это было исправлено один раз, затем повторилось, а затем снова было исправлено, кто знает.

singe3 20.01.2021 15:39

Можно создать новый преобразователь атрибутов для сопоставления типа столбца с желаемым типом атрибута.

@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, то предполагается, что это поле является другой вложенной проекцией (иначе), то это не так, и вы получаете исключение.

Есть два решения:

  1. Вернуть Timestamp, а затем вручную преобразовать в 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 создает новый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.

Nace 21.12.2020 12:30

Я никогда не заставлял 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

Другие вопросы по теме