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?
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.
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.
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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With