Spring - та же сущность, отсоединенная в @EventListener, но присоединенная в классе @Service - PullRequest
0 голосов
/ 26 января 2020

В моем приложении Spring есть следующие классы: TaskService и DevStartup.

Когда приложение запускается, DevStartup запускается, но Tag("Home") рассматривается как отдельная сущность в tasksRepository.save(task), которая генерирует исключение отсоединенной сущности во время запуска.

@Component
@AllArgsConstructor
@Slf4j
@Profile("dev")
public class DevStartup {

    private final TagsService tagsService;
    private final TagsRepository tagsRepository;
    private final TasksRepository tasksRepository;
    private final Clock clock;
    private final EntityManager entityManager;

    @EventListener(ApplicationReadyEvent.class)
    public void initializeApplication() {
        insertTags();
        insertTasks();
    }

    private void insertTags() {
        List<Tag> tags = Arrays.asList(
            new Tag("Home")
        );
        tagsRepository.saveAll(tags);
    }

    private void insertTasks() {
        Task task = new Task("Run a webinar", "Zoom.us", clock.time());
        Set<Tag> tagsForTask = Stream.of("Home")
            .map(tag -> tagsService.findByName(tag).orElseGet(() -> new Tag(tag)))
            .collect(Collectors.toSet());
        task.addTags(tagsForTask);          
        tasksRepository.save(task);             // ERROR -> Tag("Home") Entity Detached!!!!!
    }
}

В то же время у меня есть точный код в классе TaskService, и я вызываю его с теми же аргументами из моего контроллера REST.

addTask("Task title", "Task description", Stream.of("Home").collect(toSet());

И на этот раз Tag("Home") сущность прикреплена

@Service
@RequiredArgsConstructor
public class TasksService {
    private final StorageService storageService;
    private final TasksRepository tasksRepository;
    private final TagsService tagsService;
    private final Clock clock;
    private final EntityManager entityManager;

    public Task addTask(String title, String description, Set<String> tags) {
        Task task = new Task(
            title,
            description,
            clock.time()
        );
        Set<Tag> tagsForTask = tags.stream()
            .map(tag -> tagsService.findByName(tag).orElseGet(() -> new Tag(tag)))
            .collect(Collectors.toSet());
        task.addTags(tagsForTask);
        tasksRepository.save(task);        // OK -> Tag("Home") Entity Attached
        return task;
    }
    // ...
}

В чем разница между этими двумя и почему сущность отсоединена в одном случае и прикреплена в другом ?

Я использую Spring Boot 2.1.9 с Hibernate 5 и JPA (проект Spring Data JPA).

TagsService.findByName() просто вызывает TagsRepository.findByNameContainingIgnoreCase:

public interface TagsRepository extends JpaRepository<Tag, Long> {

    Optional<Tag> findByNameContainingIgnoreCase(String name);

}

Обновление

Вот журналы трассировки от DevStartup. Я могу определить, что сеанс закрывается сразу после извлечения тега из TagService, а затем снова открывается для сохранения задачи (вот почему я получаю исключение отсоединенной сущности).

2020-01-25 23:06:46.774 TRACE 33093 --- [  restartedMain] org.hibernate.internal.SessionImpl       : Automatically flushing session
2020-01-25 23:06:46.778 TRACE 33093 --- [  restartedMain] org.hibernate.internal.SessionImpl       : SessionImpl#afterTransactionCompletion(successful=true, delayed=false)
2020-01-25 23:06:46.779 TRACE 33093 --- [  restartedMain] org.hibernate.internal.SessionImpl       : Closing session [d8094673-13fd-4b7e-af38-4aa01afbcaf7]
2020-01-25 23:06:46.789 TRACE 33093 --- [  restartedMain] org.hibernate.internal.SessionImpl       : Opened Session [d2c0c551-6523-466f-ab07-a8c1455c62a4] at timestamp: 1579990006789
2020-01-25 23:06:46.848 DEBUG 33093 --- [  restartedMain] org.hibernate.SQL                        : select tag0_.id as id1_2_, tag0_.name as name2_2_ from tag tag0_ where upper(tag0_.name) like upper(?) escape ?
Hibernate: select tag0_.id as id1_2_, tag0_.name as name2_2_ from tag tag0_ where upper(tag0_.name) like upper(?) escape ?
2020-01-25 23:06:46.850 TRACE 33093 --- [  restartedMain] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [%Home%]
2020-01-25 23:06:46.850 TRACE 33093 --- [  restartedMain] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [CHAR] - [\]
2020-01-25 23:06:46.855 TRACE 33093 --- [  restartedMain] org.hibernate.internal.SessionImpl       : Closing session [d2c0c551-6523-466f-ab07-a8c1455c62a4]
2020-01-25 23:06:46.860 TRACE 33093 --- [  restartedMain] org.hibernate.internal.SessionImpl       : Opened Session [6caeb288-a608-4aa7-a5f9-091c69900815] at timestamp: 1579990006860
2020-01-25 23:06:46.867 DEBUG 33093 --- [  restartedMain] org.hibernate.SQL                        : insert into task (id, uuid, created_at, description, title) values (null, ?, ?, ?, ?)
Hibernate: insert into task (id, uuid, created_at, description, title) values (null, ?, ?, ?, ?)
2020-01-25 23:06:46.868 TRACE 33093 --- [  restartedMain] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [fc9c993b-eccf-45ff-b49a-12498b6e62eb]
2020-01-25 23:06:46.868 TRACE 33093 --- [  restartedMain] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [TIMESTAMP] - [2020-01-25T23:06:46.785455]
2020-01-25 23:06:46.870 TRACE 33093 --- [  restartedMain] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [Zoom.us]
2020-01-25 23:06:46.872 TRACE 33093 --- [  restartedMain] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [VARCHAR] - [Run a webinar]
2020-01-25 23:06:46.879 TRACE 33093 --- [  restartedMain] org.hibernate.internal.SessionImpl       : SessionImpl#afterTransactionCompletion(successful=false, delayed=false)
2020-01-25 23:06:46.880 TRACE 33093 --- [  restartedMain] org.hibernate.internal.SessionImpl       : Closing session [6caeb288-a608-4aa7-a5f9-091c69900815]
2020-01-25 23:06:46.900 ERROR 33093 --- [  restartedMain] o.s.boot.SpringApplication               : Application run failed

Ниже приведены журналы, когда работает код от TaskService. Сеанс не закрыт между извлечением тегов и сохранением задач.

2020-01-25 23:10:24.966 TRACE 33194 --- [  restartedMain] org.hibernate.internal.SessionImpl       : Closing session [f3ea3806-3c65-4269-81b4-ba8bc1abf2af]
2020-01-25 23:10:55.518  INFO 33194 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-01-25 23:10:55.519  INFO 33194 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2020-01-25 23:10:55.533  INFO 33194 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 14 ms
2020-01-25 23:10:55.553 TRACE 33194 --- [nio-8080-exec-1] org.hibernate.internal.SessionImpl       : Opened Session [61d3d232-2d92-4002-8e4c-cedb0278a2e9] at timestamp: 1579990255552
2020-01-25 23:10:55.858  INFO 33194 --- [nio-8080-exec-1] p.s.t.tasks.boundary.TasksController     : Storing new task: CreateTaskRequest(title=Dokończyć Moduł 8, description=Jpa i Hibernate cz. 2, attachmentComment=null, tags=[Home])
2020-01-25 23:10:55.914 DEBUG 33194 --- [nio-8080-exec-1] org.hibernate.SQL                        : select tag0_.id as id1_2_, tag0_.name as name2_2_ from tag tag0_ where upper(tag0_.name) like upper(?) escape ?
Hibernate: select tag0_.id as id1_2_, tag0_.name as name2_2_ from tag tag0_ where upper(tag0_.name) like upper(?) escape ?
2020-01-25 23:10:55.920 TRACE 33194 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [%Home%]
2020-01-25 23:10:55.920 TRACE 33194 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [CHAR] - [\]
2020-01-25 23:10:59.218 DEBUG 33194 --- [nio-8080-exec-1] org.hibernate.SQL                        : insert into task (id, uuid, created_at, description, title) values (null, ?, ?, ?, ?)
Hibernate: insert into task (id, uuid, created_at, description, title) values (null, ?, ?, ?, ?)
2020-01-25 23:10:59.218 TRACE 33194 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [bac8dacb-9424-4c6e-9a3a-860973ebc350]
2020-01-25 23:10:59.219 TRACE 33194 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [TIMESTAMP] - [2020-01-25T23:10:55.878153]
2020-01-25 23:10:59.223 TRACE 33194 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [Jpa i Hibernate cz. 2]
2020-01-25 23:10:59.224 TRACE 33194 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [VARCHAR] - [Dokończyć Moduł 8]
2020-01-25 23:10:59.244 TRACE 33194 --- [nio-8080-exec-1] org.hibernate.internal.SessionImpl       : SessionImpl#beforeTransactionCompletion()
2020-01-25 23:10:59.244 TRACE 33194 --- [nio-8080-exec-1] org.hibernate.internal.SessionImpl       : Automatically flushing session
2020-01-25 23:10:59.262 DEBUG 33194 --- [nio-8080-exec-1] org.hibernate.SQL                        : insert into tags_tasks (task_id, tag_id) values (?, ?)
Hibernate: insert into tags_tasks (task_id, tag_id) values (?, ?)
2020-01-25 23:10:59.263 TRACE 33194 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2020-01-25 23:10:59.264 TRACE 33194 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
2020-01-25 23:10:59.273 TRACE 33194 --- [nio-8080-exec-1] org.hibernate.internal.SessionImpl       : SessionImpl#afterTransactionCompletion(successful=true, delayed=false)

1 Ответ

2 голосов
/ 26 января 2020

Скорее всего, у вас включено Открыть сеанс в представлении включено

Если вы запускаете веб-приложение, Spring Boot по умолчанию регистрирует OpenEntityManagerInViewInterceptor для применения «Открыть EntityManager в представлении». Шаблон, чтобы позволить ленивую загрузку в веб-просмотров. Если вы не хотите, чтобы такое поведение, вы должны установить для spring.jpa.open-in-view значение false в вашем application.properties.

Это откроет сеанс при запуске запроса и закроет его после обработка запроса завершена. Перехватчик запускается для веб-запроса, но не в случае ApplicationReadyEvent прослушивателя событий.

Возможно, вы захотите использовать @Transactional для продления времени жизни сеанса, когда вы не полагаетесь на открытый сеанс в представлении. , См. { ссылка }:

В Spring существует взаимно-однозначное соответствие между бизнес-транзакцией, разграниченной @Transactional, и сессией гибернации.

То есть, когда бизнес-операция начинается с вызова метода @Transactional, сеанс гибернации создается (TransactionManager может отложить фактическое создание до тех пор, пока сеанс не будет впервые использован). Как только этот метод завершается, бизнес-транзакция фиксируется или откатывается, что закрывает сеанс гибернации.

Многие рассматривают открытый сеанс в качестве анти-паттерна: https://vladmihalcea.com/the-open-session-in-view-anti-pattern/

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...