0

I have following classes in my Spring application: TaskService and DevStartup.

When application starts DevStartup is run, but the Tag("Home") is seen as detached entity on tasksRepository.save(task) which throws detached entity exception during startup.

@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!!!!!
    }
}

At the same time I'm having exact code in the TaskService class and I call it with the same arguments from my REST Controller.

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

And this time, the Tag("Home") entity is attached

@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;
    }
    // ...
}

What is the difference between those two and why entity is detached in one case and attached in the other?

I am using Spring Boot 2.1.9 with Hibernate 5 and JPA (Spring Data JPA project).

TagsService.findByName() is just calling TagsRepository.findByNameContainingIgnoreCase:

public interface TagsRepository extends JpaRepository<Tag, Long> {

    Optional<Tag> findByNameContainingIgnoreCase(String name);

}

Update

Here are the trace logs from DevStartup. I can spot that session is closed right after fetching the Tag from TagService, and then opened again for saving Task (this is why I'm getting the detached entity exception).

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

Below are the logs when running code from TaskService. The session is not closed between fetching tags and saving tasks.

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)
Dariusz Mydlarz
  • 2,940
  • 6
  • 31
  • 59

1 Answers1

2

Most likely, you have Open Session In View enabled

If you are running a web application, Spring Boot by default registers OpenEntityManagerInViewInterceptor to apply the “Open EntityManager in View” pattern, to allow for lazy loading in web views. If you do not want this behavior, you should set spring.jpa.open-in-view to false in your application.properties.

This will open the session when request starts and and closes it after the request processing is done. The interceptor is run for web request, but not in case of ApplicationReadyEvent event listener.

You may want to use @Transactional to extend the lifetime of the session when you are not relying on open session in view. See https://stackoverflow.com/a/24713402/1570854:

In Spring, there is a one-to-one correspondence between the business transaction demarcated by @Transactional, and the hibernate Session.

That is, when a business transaction is begun by invoking a @Transactional method, the hibernate session is created (a TransactionManager may delay the actual creation until the session is first used). Once that method completes, the business transaction is committed or rolled back, which closes the hibernate session.

Many consider open session in view to be an anti-pattern: https://vladmihalcea.com/the-open-session-in-view-anti-pattern/

Community
  • 1
  • 1
Lesiak
  • 22,088
  • 2
  • 41
  • 65