【问题标题】:Spring - Same entity detached on the @EventListener but attached in the @Service classSpring - 在@EventListener 上分离但附加在@Service 类中的相同实体
【发布时间】:2020-01-25 21:38:54
【问题描述】:

我的 Spring 应用程序中有以下类:TaskServiceDevStartup

当应用程序启动时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;
    }
    // ...
}

这两者有什么区别,为什么实体在一种情况下是分离的,而在另一种情况下是附加的?

我正在使用带有 Hibernate 5 和 JPA(Spring Data JPA 项目)的 Spring Boot 2.1.9。

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)

【问题讨论】:

    标签: spring hibernate jpa


    【解决方案1】:

    很可能,您启用了Open Session In View

    如果您正在运行 Web 应用程序,Spring Boot 默认注册 OpenEntityManagerInViewInterceptor 以应用“在视图中打开实体管理器”模式,以允许在 Web 视图中延迟加载。如果您不希望出现这种行为,则应在 application.properties 中将 spring.jpa.open-in-view 设置为 false。

    这将在请求开始时打开会话,并在请求处理完成后关闭它。拦截器针对 Web 请求运行,但不适用于 ApplicationReadyEvent 事件侦听器。

    当您不依赖视图中的打开会话时,您可能希望使用@Transactional 来延长会话的生命周期。见https://stackoverflow.com/a/24713402/1570854:

    在Spring中,@Transactional划分的业务事务与hibernate Session之间是一一对应的。

    也就是说,当通过调用@Transactional 方法开始业务事务时,会创建休眠会话(TransactionManager 可能会延迟实际创建,直到第一次使用会话)。一旦该方法完成,业务事务将被提交或回滚,从而关闭休眠会话。

    许多人认为开放会话是一种反模式:https://vladmihalcea.com/the-open-session-in-view-anti-pattern/

    【讨论】:

      猜你喜欢
      • 2015-01-03
      • 2016-06-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-02
      • 2021-11-26
      相关资源
      最近更新 更多