Reputation: 4914
I have a problem where if I have a client call two methods on my service it fails with the transaction in the second method not having a session associated with it. But if I combine both methods into the service and call that one method from my client code it succeeds.
Can anyone explain to me why this happens?
Consider the following code:
@Configurable
public class ParentService {
@PersistenceContext
private EntityManager entityManager;
public ParentService() {
}
@Transactional
public Parent findById( Long id ) {
return entityManager.findById( Parent.class, id );
}
@Transactional
public Set<Child> getChildrenFor( Parent parent ) {
return Collections.unmodifiableSet( new HashSet<>( parent.getChildren() ) );
}
@Transactional
public Set<Child> getChildrenFor( Long id ) {
Parent parent = findById( id );
return getChildrenFor( parent );
}
...
}
So what happens here is that in my my client code (which has no knowledge of transactions) if I call #getChildrenFor(id) I am fine. But if I call:
Parent parent = service.findById( id );
Set<Child> children = service.getChildrenOf( parent );
Then hibernate throws an exception saying that there is no session associated with the current transaction so it cannot iterate through the Lazily loaded PersistentSet of #getChildren.
Now I'm not a JPA or Spring expert so maybe this is intended behavior. If so could you let me know why? Do I need to create a DTA that is not the entity for the service to expose and then have my clients use that instead of the entity? I would think since both calls are using the same entity manager reference that a client should be able to make both calls.
Btw, this is "spring-configured" using CTW. JpaTransactionManager is configured for cross cutting like so:
@Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager txnMgr = new JpaTransactionManager();
txnMgr.setEntityManagerFactory( entityManagerFactory().getObject() );
// cross cut transactional methods with txn management
AnnotationTransactionAspect.aspectOf().setTransactionManager( txnMgr );
return txnMgr;
}
Please let me know whatever additional information I can provide to help troubleshoot this problem.
Spring XML Configuration:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">
<context:spring-configured/>
<context:component-scan base-package="com.myapp"/>
</beans>
Spring Java Configuration:
@Configuration
@PropertySource( "classpath:database.properties" )
public class DatabaseConfiguration {
@Value( "${database.dialect}" )
private String databaseDialect;
@Value( "${database.url}" )
private String databaseUrl;
@Value( "${database.driverClassName}" )
private String databaseDriver;
@Value( "${database.username}" )
private String databaseUser;
@Value( "${database.password}" )
private String databasePassword;
@Bean
public static PropertySourcesPlaceholderConfigurer properties() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setPersistenceUnitName( "persistenceUnit" );
factory.setDataSource( dataSource() );
Properties props = new Properties();
props.setProperty( "hibernate.dialect", databaseDialect );
factory.setJpaProperties( props );
return factory;
}
@Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager txnMgr = new JpaTransactionManager();
txnMgr.setEntityManagerFactory( entityManagerFactory().getObject() );
// cross cut transactional methods with txn management
AnnotationTransactionAspect.aspectOf().setTransactionManager( txnMgr );
return txnMgr;
}
@Bean
public DataSource dataSource() {
final BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName( databaseDriver );
dataSource.setUrl( databaseUrl );
dataSource.setUsername( databaseUser );
dataSource.setPassword( databasePassword );
dataSource.setTestOnBorrow( true );
dataSource.setTestOnReturn( true );
dataSource.setTestWhileIdle( true );
dataSource.setTimeBetweenEvictionRunsMillis( 1800000 );
dataSource.setNumTestsPerEvictionRun( 3 );
dataSource.setMinEvictableIdleTimeMillis( 1800000 );
return dataSource;
}
}
POM:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.2</version>
<!-- NB: do not use 1.3 or 1.3.x due to MASPECTJ-90 and do not use 1.4 due to de`clare parents issue -->
<dependencies>
<!-- NB: You must use Maven 2.0.9 or above or these are ignored (see MNG-2972) -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<outxml>true</outxml>
<aspectLibraries>
<aspectLibrary>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</aspectLibrary>
</aspectLibraries>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
Upvotes: 0
Views: 800
Reputation: 28706
When working with JTA transaction, the session (i.e. more or less the entityManager) is automatically bound to the transaction. It means that entities fetched during transaction T1 are fetched in an entityManager/session that is bound with T1.
Once T1 is commited, the entityManager/session is no more attached to any transaction (since T1 is finished).
When your client do this:
Parent parent = service.findById( id );
Set<Child> children = service.getChildrenFor( parent );
parent
is fetched during T1 and so, it is bound to an entityManager
(let's call it EM1) bound with T1. But T1 is finished (it was commited when findById
return).
Since getChildrenFor
is annotated with @Transactional
: a new tx (i.e. T2) is started by the txManager. This will create a new entityManager (i.e. EM2) associated with T2. But the parent
belongs to EM1 and EM1 still not bound to any running tx.
To solve your issue you can adapt the code of this method:
@Transactional
public Set<Child> getChildrenFor( Parent parent ) {
Parent mergedParent = entityManager.merge(parent);
return Collections.unmodifiableSet( new HashSet<>( mergedParent.getChildren() ) );
}
Calling merge
will
Merge the state of the given entity into the current persistence context.
(note that persistence context is the store associated with the current entityManager)
mergedParent
now belongs to EM2, and EM2 is bound to the currently running T2, so calling mergedParent.getChildren()
shouldn't fail.
Important remark about merge
: it is important to note that merge
return a new instance and don't touch the instance passed in argument. It is a very common mistake/misunderstanding when working with JPA to think that merge
modify the instance.
At this point, I hope you understood that when you fetch the parent and children in the same tx (calling getChildrenFor( Long id )
), there is no need to merge since both (parent and children) belongs to the same entityManager.
Upvotes: 1