4

on one of our projects we encountered a problem with Spring ignoring @Transactional annotation and then failing with the following error.

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2018-09-13 15:05:18,406 ERROR [main] org.springframework.boot.SpringApplication Application run failed org.springframework.dao.InvalidDataAccessApiUsageException: No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call; nested exception is javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call at com.my.service.CacheAService.deleteShortTermCache(CacheAService.java:70) ~[classes/:na]

I found similar questions but none of the solutions applied to this case.

  • @EnableTransactionManagement is present
  • Transactional class implements an interface
  • Transactional method is public
  • Transactional method is not called internally

When I annotate CacheService with @Transactional, everything works again. But I am trying to understand why would Spring ignore @Transactional on CacheAService.

I tried logging Spring's transaction interceptor but there is no mention of CacheA. This is the only related thing that gets logged.

2018-09-13 15:05:18,242 TRACE [main] org.springframework.transaction.interceptor.TransactionInterceptor Don't need to create transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.deleteByValidity]: This method isn't transactional.

Here is the simplified code. Code is invoked during application startup by Spring's ContextRefreshedEvent.

@Service
public class CacheService implements Cache {

    @Autowired
    private CacheA cacheAService;
    @Autowired
    private CacheB cacheBService;

    @Override
    public void clearCache() {
        cacheAService.deleteShortTermCache();
        cacheBService.deleteAll();
    }
}

public interface CacheA {
    void deleteShortTermCache();
}

@Service
@Transactional(readOnly = true)
public class CacheAService implements CacheA {

    @Autowired
    private CacheARepository cacheARepository;

    @Override
    @Transactional
    public void deleteShortTermCache() {
        cacheARepository.deleteByValidity(CacheValidity.SHORT_TERM);
    }
}

public interface CacheB {
    void deleteAll();
}

@Service
@Transactional(readOnly = true)
public class CacheBService implements CacheB {

    @Autowired
    private CacheBRepository cacheBRepository;

    @Override
    @Transactional
    public void deleteAll {
        cacheBRepository.deleteAll();
    }
}

public enum CacheValidity {
    SHORT_TERM,
    LONG_TERM
}

@Repository
public interface CacheARepository extends JpaRepository<CacheItem, Integer> {
    void deleteByValidity(CacheValidity validity);
}

public enum CacheItemKey {
    AVAILABLE,
    FUTURE,
    AVAILABLE_UTM,
    FUTURE_UTM,
    REGION
}

@Entity
@Table(name = "cache_item")
public class CacheItem {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "cache_item_id_seq")
    @SequenceGenerator(name = "cache_item_id_seq", sequenceName = "cache_item_id_seq", allocationSize = 1)
    private Integer id;

    @Column(nullable = false, unique = true)
    @Enumerated(EnumType.STRING)
    private CacheItemKey key;

    @Column(nullable = false)
    private String value;

    @Column(name = "date_modified", nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private Date dateModified;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private CacheValidity validity;

    public Integer getId() {
        return id;
    }

    public void setId(final Integer id) {
        this.id = id;
    }

    public CacheItemKey getKey() {
        return key;
    }

    public void setKey(final CacheItemKey key) {
        this.key = key;
    }

    public String getValue() {
        return value;
    }

    public void setValue(final String value) {
        this.value = value;
    }

    public Date getDateModified() {
        return dateModified;
    }

    public void setDateModified(final Date dateModified) {
        this.dateModified = dateModified;
    }

    public CacheValidity getValidity() {
        return validity;
    }

    public void setValidity(final CacheValidity validity) {
        this.validity = validity;
    }

}

Edit: After some digging I found this in the logs.

2018-09-14 06:24:11,174 INFO [localhost-startStop-1] org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker Bean 'cacheAService' of type [com.my.service.CacheAService] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

Slava Vedenin
  • 58,326
  • 13
  • 40
  • 59
Januson
  • 4,533
  • 34
  • 42
  • Code of `CacheA`, `CacheB`? – NiVeR Sep 13 '18 at 13:53
  • I added the interfaces. – Januson Sep 13 '18 at 14:01
  • 1. `deleteByValidity` should be annotated with `@Transactional`, as it triggers a modifying operation. 2. Try `@EnableTransactionManagement(proxyTargetClass = true)`, since class methods annotated with `@Transactional` implement interface methods, which themselves have not been annotated with `@Transactional`. Alternatively, move `@Transactional` to the method declarations in interfaces `CacheA` and `CacheB`. – manish Sep 14 '18 at 03:20

3 Answers3

2

We discovered that this issue was caused by Spring Boot's auto-configuration. Since auto-configuration already setups the Transaction management, our custom configuration of @EnableTransactionManagement broke the instantiation of transaction advisors. Removing of @EnableTransactionManagement from our configuration solves the issue.

Januson
  • 4,533
  • 34
  • 42
  • { 5h later... } confirm solution in my case (more complex setup with kotlin / spring boot - but already debugged that to few methods were being scanned via spring aop utils) – wendro Mar 13 '19 at 19:16
1

Try to use only one Transactional annotation (in class or method). May be, the problem is with @Transactional(readOnly = true), because you transaction isn't readOnly, I can't sure what Transactional annotation is preferred by Spring. Try to use:

@Service
public class CacheAService implements CacheA {

    @Autowired
    private CacheARepository cacheARepository;

    @Override
    @Transactional
    public void deleteShortTermCache() {
        cacheARepository.deleteByValidity(CacheValidity.SHORT_TERM);
    }
}

or

@Service
@Transactional
public class CacheAService implements CacheA {

    @Autowired
    private CacheARepository cacheARepository;

    @Override
    public void deleteShortTermCache() {
        cacheARepository.deleteByValidity(CacheValidity.SHORT_TERM);
    }
} 
Slava Vedenin
  • 58,326
  • 13
  • 40
  • 59
  • Thank you for your answer. Unfortunately this does not work. I tried both variants. Strange thing is that only CacheA does not work. When I comment it out cache B works as expected. – Januson Sep 13 '18 at 14:30
  • Can you add code CacheARepository, CacheBRepository and code Entities that is uses in this Repository. Do you have "fetchtype.lazy" or cascade deleted like "@OneToMany (cascade = CascadeType.ALL, ...)" in entities in CacheARepository? – Slava Vedenin Sep 13 '18 at 14:54
  • I added the code. No there are just values. No other entities. – Januson Sep 13 '18 at 15:19
0

I ran in a familiar situation. For those, who have same problems but the answers won't work.
The @Transactional-annotation works per default only on public-methods. Not on private and not on package-private.

See this question for more information and workarounds -> Does Spring @Transactional attribute work on a private method?

akop
  • 5,981
  • 6
  • 24
  • 51