0

我有一个问题,如果我有一个客户端在我的服务上调用两个方法,它会失败,第二种方法中的事务没有与之关联的会话。但是,如果我将这两种方法结合到服务中并从我的客户端代码中调用该方法,它就会成功。

谁能向我解释为什么会这样?

考虑以下代码:

@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 );
    }

    ...
}

所以这里发生的是,如果我调用 #getChildrenFor(id) 在我的客户端代码(不了解事务)中我很好。但如果我打电话:

   Parent parent = service.findById( id );
   Set<Child> children = service.getChildrenOf( parent );

然后hibernate抛出一个异常,说没有与当前事务关联的会话,因此它无法遍历#getChildren的延迟加载的PersistentSet。

现在我不是 JPA 或 Spring 专家,所以也许这是预期的行为。如果是这样,你能告诉我为什么吗?我是否需要创建一个不是服务要公开的实体的 DTA,然后让我的客户使用它而不是实体?我认为由于两个调用都使用相同的实体管理器引用,因此客户端应该能够进行这两个调用。

顺便说一句,这是使用 CTW 的“弹簧配置”。JpaTransactionManager 配置为横切,如下所示:

@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;
}

请让我知道我可以提供的任何其他信息来帮助解决此问题。

春天 XML 配置:

<?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>

春季Java配置:

@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;
    }
}

聚甲醛:

    <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>
4

1 回答 1

1

使用 JTA 事务时,会话(即或多或少的 entityManager)自动绑定到事务。这意味着在事务 T1 期间获取的实体是在与 T1 绑定的 entityManager/session 中获取的。

一旦提交 T1,entityManager/session 就不再附加到任何事务(因为 T1 已完成)。

当您的客户这样做时:

Parent parent = service.findById( id );
Set<Child> children = service.getChildrenFor( parent );

parent在 T1 期间获取,因此它绑定到entityManager与 T1 绑定的(我们称之为 EM1)。但是 T1 已完成(findById返回时已提交)。

由于getChildrenFor注释为@Transactional:一个新的 tx(即 T2)由 txManager 启动。这将创建一个与 T2 关联的新 entityManager(即 EM2)。但是parent属于 EM1 并且 EM1 仍然没有绑定到任何正在运行的 tx。

要解决您的问题,您可以调整此方法的代码:

@Transactional
public Set<Child> getChildrenFor( Parent parent ) {
   Parent mergedParent = entityManager.merge(parent);
   return Collections.unmodifiableSet( new HashSet<>( mergedParent.getChildren() ) );
}

呼唤merge意志

将给定实体的状态合并到当前持久化上下文中。

(注意持久化上下文是与当前 entityManager 关联的store )

mergedParent现在属于 EM2,并且 EM2 绑定到当前正在运行的 T2,所以调用mergedParent.getChildren()应该不会失败。

关于的重要merge说明:重要的是要注意merge返回一个新实例并且不要触摸传入参数的实例。在使用 JPA 时认为merge修改实例是一个非常常见的错误/误解。

至此,我希望您明白,当您在同一个 tx(调用getChildrenFor( Long id ))中获取父子节点和子节点时,不需要合并,因为(父节点和子节点)都属于同一个 entityManager。

于 2013-01-30T09:38:06.900 回答