Ошибка SQL: 23503, SQLState: 23503 в SpringBoot 2.1.4.RELEASE app

#spring #jpa #spring-data-jpa #spring-data

#весна #jpa #spring-data-jpa #весна-данные

Вопрос:

У меня есть приложение веб-службы SpringBoot 2.1.4.RELEASE RESTful., использующее инициализатор Spring, встроенный Tomcat, механизм шаблонов Thymeleaf и пакет в виде исполняемого файла JAR.

У меня есть этот класс:

 @Entity
@Table(name="t_menu_alert_notification")
public class MenuAlertNotification implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;


    public MenuAlertNotification() {
        super();
    }


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonProperty("id")
    private Long id;    

    @JsonProperty("subject")
    protected String subject;

    @JsonIgnore
    protected Integer trend;

    @JsonIgnore
    protected String message;

    @JsonProperty("notified")
    private Boolean notified;

    @Column(name = "is_read")
    protected Boolean read;

    @JsonProperty("creationDate")
    @Convert(converter = LocalDateTimeAttributeConverter.class)
    protected LocalDateTime creationDate;


    @ManyToOne(fetch = FetchType.EAGER)
        @JoinColumn(name = "menu_alert_id")
    @JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="name")
    @JsonIdentityReference(alwaysAsId=true)
    protected MenuAlert menuAlert;
..
}
 

и этот метод в репозитории:

   @Transactional(propagation =Propagation.REQUIRED,
                    isolation=Isolation.SERIALIZABLE,
                    readOnly=false,
                    transactionManager="transactionManager")
    @Modifying
        @Query("update MenuAlertNotification n set n.read = :status where n.id in :notificationIdList and n.menuAlert.menu.user.id = :userId")
        void changemenuNotificationListReadStatus(  @Param("notificationIdList") List<Long> notificationIdList, 
                                                        @Param("userId") long userId, 
                                                        @Param("status") boolean status);
 

Я создал тест Junit :

 MenuAlertNotification menuAlertNotification = new MenuAlertNotification (menuAlert);        
        menuAlertNotificationService.save(menuAlertNotification);               
        assertFalse (menuAlertNotification.getRead());      
        Long menuAlertNotificationId = menuAlertNotification.getId();       
        List<Long> notificationIdList = new ArrayList <Long>();     
        notificationIdList.add  (menuAlertNotificationId);

        menuAlertNotificationService
                .changeMenuNotificationReadStatus (notificationIdList, user.getId(), Boolean.TRUE);
 

когда я сохраняю объект, все в порядке, но когда я вызываю метод changeMenuNotificationReadStatus , я получаю эту ошибку:

 019-04-16 11:21  [main] WARN  o.h.e.jdbc.spi.SqlExceptionHelper.logExceptions(137) - SQL Error: 23503, SQLState: 23503
2019-04-16 11:21  [main] ERROR o.h.e.jdbc.spi.SqlExceptionHelper.logExceptions(142) - Referential integrity constraint violation: "FK56KKTN0YV9SJJIOJJVJBPAGNW: PUBLIC.T_MENU_ALERT_NOTIFICATION FOREIGN KEY(MENU_ALERT_ID) REFERENCES PUBLIC.T_MENU_ALERT(ID) (1)"; SQL statement:
delete from t_menu_alert where id=? [23503-197]
 

Комментарии:

1. я думаю, проблема заключается в «n.menuAlert.menu.user.id = :Идентификатор пользователя», потому что вы являетесь собственностью access дочернего элемента. и сравнивая его с id, вот почему возникает эта проблема. Поскольку MenuAlert стремится, поэтому он извлекается, но его дочерний элемент не извлекается, как дочерний пользователь menu и menu, их тип выборки будет ленивым. Поскольку они не извлекаются во время выполнения, эта проблема может возникнуть.

2. Я предполагаю, что вы создаете новый menuAlert в своем модульном тестировании, и платформа тестирования пытается удалить его в конце вашего теста. Можете ли вы включить весь модульный тест с аннотациями к тесту.

3. Да, пожалуйста, добавьте весь тест, если наши ответы не решили проблему.

4. Пожалуйста, посмотрите на мой ответ и отметьте его соответствующим образом, если это полезно

Ответ №1:

Идентификатор не генерируется MenuAlertNotification menuAlertNotification = new MenuAlertNotification (menuAlert); , когда вы Long menuAlertNotificationId = menuAlertNotification.getId(); это делаете, и он всегда будет возвращаться null .

Вы должны изменить вторую строку вашего теста junit на

 menuAlertNotification = menuAlertNotificationService.save(menuAlertNotification);
 

Я предполагаю, что ваша служба menuAlertNotificationService.save(menuAlertNotification); возвращает что-то вроде return notificationRepo.save(entity) .

Таким образом, у вас есть новый объект, заполненный идентификатором после вставки строки, и, следовательно, он не получит указанное исключение.

Ответ №2:

Проблема заключается в том, что вы используете базу данных в памяти, и когда метод тестирования завершен, Junit удаляет все таблицы, но не в правильном порядке.

Код уровня сервиса должен зависеть от репозитория. Однако для тестирования уровня обслуживания вам не нужно знать или заботиться о том, как реализован уровень сохраняемости.

В идеале вы должны иметь возможность писать и тестировать наш код уровня обслуживания без подключения к нашему полному уровню сохраняемости.

Для достижения этой цели вы можете использовать поддержку mocking, предоставляемую Spring Boot Test.

Ответ №3:

Ошибка вызвана тем, что база данных собирается удалить запись, на которую ссылаются другие записи с внешним ключом.(Ограничение внешнего ключа)

Я предполагаю, что в тестовом примере перед фрагментом кода, который вы проанализировали здесь, есть код операции удаления, что-то вроде

 MenuAlertRepository.deleteAll()
 

Spring JPA не будет выполнять эту инструкцию немедленно при наличии транзакции. После этого Spring JPA выполнил запрос JPQL (changeMenuNotificationReadStatus), затем он сбросил все кэшированные операторы в БД. В это время оператор delete выполнялся точно. Затем было выброшено исключение ограничения внешнего ключа.

Ответ №4:

Как сказал Цзянь, ошибка возникает не из-за вызова changeMenuNotificationReadStatus, а, вероятно, из-за предыдущего оператора, который не был сброшен.

Что-то генерирует этот оператор удаления, который отвечает за нарушение ограничения FK:

 delete from t_menu_alert where id=?
 

В ваших тестах вы могли бы использовать saveAndFlush методы вместо save , и flush регулярно отправлять запросы на изменение SQL и видеть, в чем проблема.

Здесь JPA пытается удалить MenuAlert перед удалением ссылки MenuAlertNotification , поэтому ограничение FK блокируется, как вы, вероятно, знаете.

Поскольку уведомление не имеет смысла для удаленного предупреждения, вы также можете каскадировать удаление:

 @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
 

или измените ON DELETE действие ограничения FK на уровне базы данных: КАСКАД удалит ссылки, MenuAlertNotification а SET NULL очистит ассоциацию, установив значение null для ссылки MenuAlertNotification.menuAlert .

Вы также можете написать Java-код для удаления MenuAlertNotification s перед MenuAlert , на основе условий.

Мне было бы любопытно прочитать весь ваш тестовый код.

Адиль Халил, ты прав, что если метод Spring Data JPA Repository.save используется напрямую, нам нужно получить возвращаемое значение с обновленным идентификатором. Но здесь, я думаю, он вызывает сервисный уровень, который должен возвращать обновленное значение:

 @Service
public class MenuAlertNotificationService {
    private MenuAlertNotificationRepository repository;

    public MenuAlertNotification save(MenuAlertNotification entity) {
        return repository.save(entity);
    } 
}