Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Transactional method not work on separated Thread

I have a Spring-based application that has a background polling service runs in a separated thread to update status of data in database (EmployeeStatusPollService). I use JPA (Hibernate vendor) for the Repository. I implemented this service in two solutions, but only one solution works. Below are the two solutions.

Solution 1: Transactional method checkAndUpdateStatus in other service class and poll service calls it

@Service
public class EmployeeStatusPollService implements Runnable {

  @Inject 
  private EmployeeService employeeService;

  private static final int DEFAULT_PAGE_SIZE = 300;
  private boolean flag = true;

  public EmployeeStatusPollService() {
  }

  @PostConstruct
  public void start() {
    SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
    executor.setConcurrencyLimit(SimpleAsyncTaskExecutor.NO_CONCURRENCY);
    executor.execute(this);
  }

  public void run() { 
    while(flag) {
      try {
        int pagenum = 1;
        List<Employee> items = null;

        do {        
          items = employeeService.checkAndUpdateStatus(pagenum, DEFAULT_PAGE_SIZE);
        } while(items != null && items.size() == DEFAULT_PAGE_SIZE);      
      } catch(Exception ex) {

      }
    }
  }
}

@Service
public class EmployeeServiceImpl implements EmployeeService {

  private static final Logger LOG = LoggerFactory.getLogger(EmployeeServiceImpl.class);

  @Inject 
  private EmployeeRepository employeeRepository;

  @Transactional
    public List<Employee> checkAndUpdateStatus(int pagenum, int pagesize) throws Exception {
    // ....
  }
}

Solution 2: Transactional method checkAndUpdateStatus is in poll service class

@Service
public class EmployeeStatusPollService implements Runnable {

  @Inject 
  private EmployeeRepository employeeRepository;

  private static final int DEFAULT_PAGE_SIZE = 300;
  private boolean flag = true;

  public EmployeeStatusPollService() {
  }

  @PostConstruct
  public void start() {
    SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
    executor.setConcurrencyLimit(SimpleAsyncTaskExecutor.NO_CONCURRENCY);
    executor.execute(this);
  }

  public void run() { 
    while(flag) {
      try {
        int pagenum = 1;
        List<Employee> items = null;

        do {        
          items = checkAndUpdateStatus(pagenum, DEFAULT_PAGE_SIZE);
        } while(items != null && items.size() == DEFAULT_PAGE_SIZE);      
      } catch(Exception ex) {

      }
    }
  }

  @Transactional
    public List<Employee> checkAndUpdateStatus(int pagenum, int pagesize) throws Exception {
    // ....
  }
}

Details of method checkAndUpdateStatus

@Transactional
public List<Employee> checkAndUpdateStatus(int pagenum, int pagesize) throws Exception {
  PageRequest page = new PageRequest(pagenum, pagesize);
  Page<Employee> pagedItems = employeeRepository.findByStatus(EmployeeStatus.PENDING, page);  // Line 1: Query employees 
  List<Employee> emps = pagedItems.getContent();      
  List<Long> updatedItems = new ArrayList<>();
  int i = 0;

  for(Employee emp:emps) {
    try {
      // ...

      emp.setStatus(status);  // Line 2: Update employee's status
      employeeRepository.save(emp); // Line 3: Save/Update employee
      updatedItems.add(emp.getId());
      i++;

      if(i % 50 == 0) {
        employeeRepository.flush(); // Line 4: flush for every 50 employees
      }

      //....        
    } catch (Exception ex) {    
      // handle exception here....
    }
  }

  return emps;
}

Configuration

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>

<tx:annotation-driven transaction-manager="transactionManager" />

<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
    <property name="driverClassName" value="${jdbc.driver}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
    <property name="defaultAutoCommit" value="false" />
</bean>

<bean id="entityManager" class="org.springframework.orm.jpa.support.SharedEntityManagerBean">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource"></property>
    <property name="jpaDialect" ref="jpaDialect"></property>
    <property name="jpaVendorAdapter" ref="jpaVendorAdapter"></property>
    <property name="packagesToScan" value="${jpa.packagesToScan}" />
    <property name="jpaProperties">
        <props>
            <prop key="hibernate.dialect">${hibernate.dialect}</prop>
            <prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
            <prop key="hibernate.format_sql">${hibernate.format_sql}</prop>
            <prop key="hibernate.connection.autocommit">${hibernate.connection.autocommit}</prop>
            <prop key="hibernate.connection.defaultAutoCommit">${hibernate.connection.defaultAutoCommit}</prop>
        </props>
    </property>
</bean>

<bean id="jpaDialect" class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"></bean>

<bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"></bean>

Solution 2 does not work and I get the error "Persistent entity is detached..." when it updates/saves the entity to the database at the Line 3 of method checkAndUpdateStatus.

IMO, Solution 2 does not work because the method checkAndUpdateStatus isn't being put in Transactional Context although it's marked by @Transactional. Even when I set REQUIRES_NEW it still doesn't work. Could anyone can explain me about this error why it happened or send me some reference documentation about this?

like image 873
Barcelona Avatar asked Nov 28 '14 04:11

Barcelona


People also ask

How Spring transactions use multiple threads?

This can be achieved by keeping transaction at thread level. However u would require a shared object between all those threads. This shared object will keep a track of status of all this threads and notify them if either fails.

Does @transactional works on private method?

The answer your question is no - @Transactional will have no effect if used to annotate private methods. The proxy generator will ignore them. When using proxies, you should apply the @Transactional annotation only to methods with public visibility.

How does Spring @transactional really work?

So when you annotate a method with @Transactional , Spring dynamically creates a proxy that implements the same interface(s) as the class you're annotating. And when clients make calls into your object, the calls are intercepted and the behaviors injected via the proxy mechanism.

How are transactions implemented in multi threaded environment?

Your parent processing thread will (1.) create new threads to do parallel DB inserts, (2.) create a CountDownLatch object (this will hold your parent thread until all child threads which are doing db inserts are finished) (3.) create a db connection object with auto commit mode as FALSE.


1 Answers

Transactional annotation works by creating a proxy on the original class, and local or internal calls to methods within a class do not cause the proxied method to be invoked. In other words, a call from a method in a class to another method in the same class does not get intercepted by the proxy.

To explain that specifically in your case:

For transactional annotation to work, spring creates a proxy around EmployeeStatusPollService class that will wrap your EmployeeStatusPollService class instance to intercept calls to checkAndUpdateStatus method that has the transactional annotation.

The proxied version of the checkAndUpdateStatus method adds the desired transactional behaviour and afterwards calls the original checkAndUpdateStatus method. This has the effect however, that any call to the checkAndUpdateStatus method from within your class instance is directly invoked on that instance and will not be intercepted by the wrapping proxy.

Therefore, in the second example, when the "checkAndUpdateStatus" is called internally from another method of the EmployeeStatusPollService, it is not called on the proxied version of the EmployeeStatusPollService that has transactional capability, but on the normal one.

The first example worked because the checkAndUpdateStatus was in a different class and therefore the external call was intercepted by the proxy.

You can read more about the fact that transactional works with the creation of proxies here: https://spring.io/blog/2012/05/23/transactions-caching-and-aop-understanding-proxy-usage-in-spring

And you can read here about how the proxying procedure works: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html#aop-understanding-aop-proxies

like image 141
Marios Avatar answered Sep 28 '22 16:09

Marios