简体   繁体   中英

Multiple JTA Transactions: no session associated with the current transaction

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>

When working with JTA transaction, the session (ie 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 (ie T2) is started by the txManager. This will create a new entityManager (ie 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM