Spring Data @Async Save выбрасывает PersistentObjectException: detached entity passed to persist

 
 
 
Сообщения:38
При попытке вызвать асинхронно сохранение в JPA(Spring Data) получаю PersistentObjectException: detached entity passed to persist
Если все делать не создавая новый поток - все отлично работает, но задача состоит в том что бы не дожидаться сохранения и возвращать статистику.
На сколько я понимаю проблема в каскадах, но решить проблему так и не смог

Модель
Деление на 3 сущности исключительно в целях НЕ ДУБЛИРОВАНИЯ длинных строк в базе
@Entity(name = "event")
public class Event {
    @Id
    @Column(unique = true, nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private LocalDateTime dateTime;
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "user_id")
    private User user;
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "url_id")
    private Url url;

    public Event() {
    }

    public Event(LocalDateTime dateTime, User user, Url url) {
        this.dateTime = dateTime;
        this.user = user;
        this.url = url;
    }
//...getters, setters, equals, etc.


@Entity
public class Url {
    @Id
    @Column(unique = true, nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(unique = true, nullable = false)
    private String url;

    public Url() {
    }


@Entity
public class User {
    @Id
    @Column(unique = true, nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(unique = true, nullable = false)
    private String uid;

    public User() {
    }

Контроллер
    
@PostMapping("/events")
    public String addEvent(@RequestParam("uid") String uid,
                           @RequestParam("url") String urlId) throws JsonProcessingException {
        User user = userService.findByUid(uid);
        if (user == null)
            user = new User(uid);

        Url url = urlService.findByUrl(urlId);
        if (url == null)
            url = new Url(urlId);
        eventService.save(new Event(LocalDateTime.now(), user, url));
        int totalVisitsCurrent24Hour = eventService.countAllByLast24Hour();
        int uniqueUsersCurrent24Hour = eventService.countUniqueUserByLast24Hour();
        return new ObjectMapper().writeValueAsString(new EventResponse24Dto(totalVisitsCurrent24Hour, uniqueUsersCurrent24Hour));


Сервис с асинхронным методом save(Event e), не зависимо от вызова через new Thread или средствами Spring @Async получаем Exception
@Service
public class EventServiceImpl implements EventService {
    @Autowired
    private EventRepository repository;
    @Async(value = "threadPoolTaskExecutor")
    @Transactional
    @Override
    public void save(Event e) {
//        new Thread(() -> repository.save(e)).start();
        repository.save(e);
    }
    @Override
    public int countAllByDateTimeBetween(LocalDateTime from, LocalDateTime to) {
        return repository.countAllByDateTimeBetween(from, to);
    }

    @Override
    public int countUniqueUsersByDateTimeBetween(LocalDateTime from, LocalDateTime to) {
        return repository.countUniqueUsersByDateTimeBetween(from, to);
    }

    @Override
    public int countAllByLast24Hour() {
        return countAllByDateTimeBetween(LocalDateTime.now().minusDays(1), LocalDateTime.now());
    }

    @Override
    public int countUniqueUserByLast24Hour() {
        return countUniqueUsersByDateTimeBetween(LocalDateTime.now().minusDays(1), LocalDateTime.now());
    }


Сам стек
Quote:
Hibernate: update hibernate_sequence set next_val= ? where next_val=?
Exception in thread "Thread-12" org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: ru.leodev.demo.statdigger.model.User; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: ru.leodev.demo.statdigger.model.User
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:280)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:225)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:135)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
at com.sun.proxy.$Proxy110.save(Unknown Source)
at ru.leodev.demo.statdigger.service.EventServiceImpl.lambda$save$0(EventServiceImpl.java:22)
at java.base/java.lang.Thread.run(Thread.java:844)
Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: ru.leodev.demo.statdigger.model.User
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:124)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:806)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:773)
at org.hibernate.jpa.event.internal.core.JpaPersistEventListener$1.cascade(JpaPersistEventListener.java:80)
at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:467)
at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:392)
at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:193)
at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:126)
at org.hibernate.event.internal.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:414)
at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:252)
at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:182)
at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:125)
at org.hibernate.jpa.event.internal.core.JpaPersistEventListener.saveWithGeneratedId(JpaPersistEventListener.java:67)
at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:189)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:132)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:58)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:782)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:767)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:304)
at com.sun.proxy.$Proxy105.persist(Unknown Source)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:490)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:377)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:200)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:629)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:593)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:578)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:59)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:294)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
... 11 more


Написал тесты для проверки работы асинхронности... обратите внимания что он отработал с пустыми ID таблиц юзера и URL. Но если мы достаем сущности User и URL и из базы, инжектим их в new Event(...) тогда получаем ошибку выше. Но если убрать асинхронный вызов - все отработает правильно
    @Test
    public void AsyncAnnotationForMethodsWithVoidReturnType() throws InterruptedException {
        System.out.println("Start - Invoking an asynchronous method " + Thread.currentThread().getName());
        eventService.save(new Event(LocalDateTime.now(), new User("99999999999999"), new Url("99999999999999")));
        System.out.println("End - Invoking an asynchronous method " + Thread.currentThread().getName());
        Thread.sleep(300);
    }

    @Test
    public void AsyncAnnotationForMethodsWithVoidReturnTypeWithChildrenTableIds() throws InterruptedException {
        System.out.println("Start - Invoking an asynchronous method " + Thread.currentThread().getName());
        // если изначально мы добавляем сущность сами в этом же контексте, все отработает
        eventService.save(new Event(LocalDateTime.now(), new User("99999999999999"), new Url("99999999999999"))); // save new entity
        //get saved entity
        User user = userService.findByUid("99999999999999");
        Url url = urlService.findByUrl("99999999999999");
        eventService.save(new Event(LocalDateTime.now(), user, url)); // save already existed entity
        System.out.println("End - Invoking an asynchronous method " + Thread.currentThread().getName());
        Thread.sleep(300);
    }

    @Test
    public void AsyncAnnotationForMethodsWithVoidReturnTypeWithChildrenTableIdsBad() throws InterruptedException {
        System.out.println("Start - Invoking an asynchronous method " + Thread.currentThread().getName());
        // Если не добавлять сущность, но мы знаем что она есть в базе 100% будет
        // InvalidDataAccessApiUsageException: detached entity passed to persist: ru.leodev.demo.statdigger.model.Url
//        eventService.save(new Event(LocalDateTime.now(), new User("99999999999999"), new Url("99999999999999"))); // save new entity
        //get saved entity
        User user = userService.findByUid("99999999999999");
        Url url = urlService.findByUrl("99999999999999");
        eventService.save(new Event(LocalDateTime.now(), user, url)); // save already existed entity
        System.out.println("End - Invoking an asynchronous method " + Thread.currentThread().getName());
        Thread.sleep(300);
    }
Изменен:18 мая 2018 17:33
 
 
Сообщения:9731
Видимо ты используешь OpenSessionInView Filter или Inetceptor который открывает Хиб сессию, которая привязывается к потоку.
User user = userService.findByUid(uid);
На этом этапе пользователь еще в сессии.
eventService.save(new Event(LocalDateTime.now(), user, url));
А вот здесь, раз метод асинхронен и поток уже другой, то и сессия будет открываться заново (потому что метод помечен как @Transactional). Однако пользователь который в Event'e - он от другой сессии, поэтому текущая уже считает что объект не ее и не может сохранить.

От проблемы с сессией можно избавиться убрав каскад. Но код от этого не станет рабочим - остаются проблемы с синхронизацией. В таком виде код остается не thread safe - один поток может видеть одни данные в ентитях, другой - другие.
Изменен:19 мая 2018 22:52
 
 
Сообщения:38
Спасибо, Староверъ.
Я так понимаю что нужно в дао сделать метод который начинает и заканчивает транзакцию
Его дергать в асинк потоке и все хиберовское делать в нем, поправьте если я ошибаюсь
 
 
Сообщения:9731
Транзакции как правило начинаются все же на уровне сервисов, хотя в данном случае это не так принципиально. Где бы они не начинались - главное чтоб объекты которые создались в одном потоке не проникли в другие. Т.е. если User нужен - пусть он в том же потоке и вытягивается из БД.
Изменен:20 мая 2018 14:51
 
Модераторы:Нет
Сейчас эту тему просматривают:Нет