Spring data JPA: transaction deferred requests
I looked at the issue when trying to run @NamedQuery with paging from the Spring data repository. The entity class looks like this:
@NamedQueries({
@NamedQuery(
name = "Customer.findByNamePattern",
query = "select c from Customer c where c.name like :pattern"
)
})
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
private Long id;
private String name;
Repository interface:
public interface CustomerRepository extends JpaRepository<Customer, Long> {
//@Query("select c from Customer c where c.name like :pattern")
Page<Customer> findByNamePattern(@Param("pattern") String pattern,Pageable pageable);
}
When I try to call the methods of the checked out repository from a non-transactional context (junit), it works fine.
When I call it from a transactional service method like:
@Service("customerService")
@Transactional
public class CustomerServiceImpl implements CustomerService {
private static Logger log = Logger.getLogger( CustomerServiceImpl.class.getName());
@Autowired
private CustomerRepository customerRepository;
@Transactional(readOnly = true)
public Page<Customer> findAllPaged(int pageNum, int pageSize) {
PageRequest pr = new PageRequest(pageNum,pageSize);
return customerRepository.findAll(pr);
}
@Transactional(readOnly = true)
public Page<Customer> findByNamePatternPaged(String keyword, int pageNum, int pageSize) {
PageRequest pr = new PageRequest(pageNum,pageSize);
String pattern = "%"+keyword+"%";
return customerRepository.findByNamePattern(pattern, pr);
}
... the call findAllPaged()
works fine again.
However, when I try to call a method that is supposed to use a named query, I always get an exception:
javax.persistence.RollbackException
org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:524)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:757)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:726)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:478)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:272)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207)
at com.sun.proxy.$Proxy35.findByNamePatternPaged(Unknown Source)
at datapagedquery.service.TestCustomerService.testFindByPatternPaged(TestCustomerService.java:36)
...
Caused by: javax.persistence.RollbackException: Transaction marked as rollbackOnly
at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:74)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:515)
... 33 more
Using annotation org.springframework.data.jpa.repository.Query
in a repository method works fine again from a transactional context.
After a while of debugging, it seems that the problem is caused
org.springframework.data.jpa.repository.query.NamedQuery
in doCreateCountQuery()
and hasNamedQuery()
:
@Override
protected TypedQuery<Long> doCreateCountQuery(Object[] values) {
EntityManager em = getEntityManager();
TypedQuery<Long> countQuery = null;
if (hasNamedQuery(em, countQueryName)) {
countQuery = em.createNamedQuery(countQueryName, Long.class);
} else {
Query query = createQuery(values);
String queryString = extractor.extractQueryString(query);
countQuery = em.createQuery(QueryUtils.createCountQueryFor(queryString, countProjection), Long.class);
}
return createBinder(values).bind(countQuery);
}
private static boolean hasNamedQuery(EntityManager em, String queryName) {
try {
em.createNamedQuery(queryName);
return true;
} catch (IllegalArgumentException e) {
LOG.debug("Did not find named query {}", queryName);
return false;
}
}
It is trying to create TypedQuery
from a generated name Customer.findByNamePattern.count
that does not exist in the repository with the request name EntityManager. hasNamedQuery()
checks it, catches it thrown IllegalArgumentException
, and creates it differently. The problem is that, although captured IllegalArgumentException
, the transaction is rolled back (sometimes!)
I found the following workarounds:
-
using annotation
org.springframework.data.jpa.repository.Query
for repository method -
OR - create another named query
@NamedQuery( name = "Customer.findByNamePattern.count", query = "select count(c.id) from Customer c where c.name like :pattern" ),
What is not clear to me:
- the call
findAll()
should cause the same problem, but it doesn't. What for? - using
org.springframework.data.jpa.repository.Query
instead@NamedQuery
also doesn't cause a problem, why? - How can I use @NamedQuery with a paged parameter from the transaction context to avoid the problem (and not explicitly create a count query)?
Any help would be appreciated!
UPDATE
versions used: Spring: 4.0.5.RELEASE spring-data: 1.6.0.RELEASE, 1.7.0.RELEASE Hibernate: 4.3.5. The final
After reading a similar bug in [ https://jira.spring.io/browse/DATAJPA-442] , I downgraded the hibernate version to 4.2.15.Final which fixed the problem. However, the question is still alive, is it possible to solve the problem without changing the Hibernate version?
source to share
I added a PR with a potential fix: https://github.com/spring-projects/spring-data-jpa/pull/110 We are using a new (throwaway) EntityManager to execute a named query request so the original EntityManager is not affected by unsuccessful search. As it turns out, your problem is quite difficult to reproduce. Could you give it a spin? Perhaps you could even provide a small test case for this?
source to share
The problem you are facing is driven by several artifacts:
By definition, JPA EntityManager
should be closed (and potentially re-created) after it throws an exception. This usually happens when the object's operations fail, and you can be sure of the status EntityManager
. For a simple named query search, this is pretty strict as it doesn't need to create a new one EntityManager
. However, we need to deal with this.
However, we are already working on this issue for manual queries (which is why you see it working for @Query
). However, the protection mechanism we introduced for DATAJPA-350 is not applied to the named part of the request. I have created DATAJPA-617 for you.
source to share