Spring 作用域代理事务可以通过 JPA 进行,但不能通过 JDBC 提交

发布于 2025-01-05 04:32:40 字数 3844 浏览 0 评论 0原文

我遇到一种情况,我必须在一个应用程序中处理多个客户端,并且每个客户端都有单独的数据库。为了支持我使用 Spring 自定义范围,与内置请求范围非常相似。用户在每个请求中进行身份验证,并可以根据传递的凭据设置上下文客户端 ID。范围界定本身似乎运行正常。

因此,我使用自定义范围为我的 DataSource 创建范围代理,以支持每个客户端的不同数据库。我可以连接到正确的数据库。

我为 EntityManagerFactory 创建了一个作用域代理来使用 JPA。而且这部分看起来也还可以。

我为 PlatformTransactionManager 添加了一个作用域代理来进行声明性事务管理。我在我的服务层上使用@Transactional,它可以很好地传播到我的 SpringData 支持的存储库层。

只要我只使用 JPA,一切都很好并且可以正常工作。我什至可以在请求中将上下文切换到不同的客户端(我在后台使用 ThreadLocals),并且两个数据库的事务都得到正确处理。

当我尝试在我的自定义存储库之一中使用 JDBCTempate 时,问题就出现了。乍一看一切看起来都还不错,因为没有抛出异常。但是,当我检查数据库中是否存在我认为是使用基于 JDBC 的自定义存储库插入的对象时,它们并不存在!

我确信我可以通过仅声明 JpaTransactionManager 并将 DataSourceEntityManagerFactory 传递给它来一起使用 JPA 和 JDBC - 我检查了它并没有作用域代理并且它可以工作。

所以问题是,当我对 DataSourceEntityManagerFactory 进行范围代理时,如何使用 JpaTransactionManager 使 JDBC 与 JPA 一起工作PlatformTransactionManager beans? 我提醒一下,仅使用 JPA 效果很好,但添加普通 JDBC 则不起作用。


UPDATE1:还有一件事:所有只读 (SELECT) 操作也可以在 JDBC 中正常工作 - 只有写入(INSERT、UPDATE、DELETE)最终不会提交或回滚。


更新2:正如@Tomasz建议的那样,我已经从EntityManagerFactoryPlatformTransactionManager中删除了作用域代理,因为它们确实不需要,并且比其他任何东西都更容易混淆。

真正的问题似乎是在事务内切换范围上下文。 TransactionSynchronizationManager 在事务开始时将事务资源(即EMFDS)绑定到线程。它能够解开作用域代理,因此它在启动事务时绑定活动作用域中的资源的实际实例。然后,当我更改事务中的上下文时,一切都会变得混乱。

看来我需要暂停活动事务并存储当前事务上下文,以便能够在进入另一个范围时清除它,使 Spring 认为它不再位于事务内,并强制它在需要时为新范围创建一个事务。然后,当离开范围时,我必须恢复之前暂停的事务。不幸的是,我还无法想出一个可行的实施方案。任何提示表示赞赏。


下面是我的一些代码,但它非常标准,除了范围代理之外。

DataSource

<!-- provides database name based on client context -->
<bean id="clientDatabaseNameProvider"
    class="com.example.common.spring.scope.ClientScopedNameProviderImpl"
    c:clientScopeHolder-ref="clientScopeHolder"
    p:databaseName="${base.db.name}" />

<!-- an extension of org.apache.commons.dbcp.BasicDataSource that 
    uses proper database URL based on database name given by above provider -->
<bean id="jpaDataSource" scope="client"
    class="com.example.common.spring.datasource.MysqlDbInitializingDataSource"
    destroy-method="close"
    p:driverClassName="${mysql.driver}"
    p:url="${mysql.url}"
    p:databaseNameProvider-ref="clientDatabaseNameProvider"
    p:username="${mysql.username}"
    p:password="${mysql.password}"
    p:defaultAutoCommit="false"
    p:connectionProperties="sessionVariables=storage_engine=InnoDB">
    <aop:scoped-proxy proxy-target-class="false" />
</bean>

EntityManagerFactory

<bean id="jpaVendorAdapter"
    class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"
    p:database="MYSQL"
    p:generateDdl="true"
    p:showSql="true" />

<util:properties id="jpaProperties">
    <!-- omitted for readability -->
</util:properties>

<bean id="jpaDialect"
    class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" />

<bean id="entityManagerFactory"
    class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
    p:packagesToScan="com.example.model.core"
    p:jpaVendorAdapter-ref="jpaVendorAdapter"
    p:dataSource-ref="jpaDataSource"
    p:jpaDialect-ref="jpaDialect"
    p:jpaProperties-ref="jpaProperties" />

PlatformTracsactionManager

<bean id="transactionManager"
    class="org.springframework.orm.jpa.JpaTransactionManager"
    p:dataSource-ref="jpaDataSource"
    p:entityManagerFactory-ref="entityManagerFactory" />

<tx:annotation-driven proxy-target-class="false" mode="proxy"
    transaction-manager="transactionManager" />

I have a situation where I have to handle multiple clients in one app and each client has separate database. To support that I'm using Spring custom scope, quite similar to the built in request scope. A user authenticates in each request and can set context client ID based passed credentials. The scoping itself seems to be working properly.

So I used my custom scope to create a scoped-proxy for my DataSource to support a diffrent database per client. And I get connections to proper databases.

Than I created a scoped-proxy for EntityManagerFactory to use JPA. And this part also looks OK.

Than I added a scoped-proxy for PlatformTransactionManager for declarative transaction management. I use @Transactional on my service layer and it gets propagated nicely to my SpringData powered repository layer.

All is fine and works correctly as long a s I use only JPA. I can even switch context to a diffrent client within the request (I use ThreadLocals under the hood) and transactions to both databases are handled correctly.

The problems start when I try to use JDBCTempate in one of my custom repositiries. Than at first glance all looks OK too, as no exceptions are thrown. But when I check the database for the objects I thought I inserted with my custom JDBC-based repository the're not there!

I know for sure I can use JPA and JDBC together by declaring only JpaTransactionManager and passing both the DataSource and EntityManagerFactory to it - I checked it and without the scoped-proxies and it works.

So the question is how to make JDBC work together with JPA using the JpaTransactionManager when I have scoped-proxied the DataSource, EntityManagerFactory and PlatformTransactionManager beans? I remind that using only JPA works perfectly, but adding plain JDBC into the mix is not working.


UPDATE1: And one more thing: all readonly (SELECT) operations work fine with JDBC too - only writes (INSERT, UPDATE, DELETE) end up not commited or rolledback.


UPDATE2: As @Tomasz suggested I've removed scoped proxy from EntityManagerFactory and PlatformTransactionManager as those are indeed not needed and provide more confusion than anything else.

The real problem seems to be switching the scope context within a transaction. The TransactionSynchronizationManager bounds transactional resources (i.e. EMF or DS) to thread at transaction start. It has the ability to unwrap the scoped proxy, so it binds the actual instance of the resource from the scope active at the time of starting a transaction. Then when I change the context within a transaction it all gets messed up.

It seems like I need to suspend the active transaction and store aside the current transaction context to be able to clear it upon entering another scope to make Spring think it's not inside a transaction any more and to force it create one for the new scope when needed. And then when leaving the scope I'd have to restore the previously suspended transaction. Unfortunatelly I was unable to come up with a working implementation yet. Any hints appreciated.


And below is some code of mine, but it's pretty standard, except for the scoped-proxies.

The DataSource:

<!-- provides database name based on client context -->
<bean id="clientDatabaseNameProvider"
    class="com.example.common.spring.scope.ClientScopedNameProviderImpl"
    c:clientScopeHolder-ref="clientScopeHolder"
    p:databaseName="${base.db.name}" />

<!-- an extension of org.apache.commons.dbcp.BasicDataSource that 
    uses proper database URL based on database name given by above provider -->
<bean id="jpaDataSource" scope="client"
    class="com.example.common.spring.datasource.MysqlDbInitializingDataSource"
    destroy-method="close"
    p:driverClassName="${mysql.driver}"
    p:url="${mysql.url}"
    p:databaseNameProvider-ref="clientDatabaseNameProvider"
    p:username="${mysql.username}"
    p:password="${mysql.password}"
    p:defaultAutoCommit="false"
    p:connectionProperties="sessionVariables=storage_engine=InnoDB">
    <aop:scoped-proxy proxy-target-class="false" />
</bean>

The EntityManagerFactory:

<bean id="jpaVendorAdapter"
    class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"
    p:database="MYSQL"
    p:generateDdl="true"
    p:showSql="true" />

<util:properties id="jpaProperties">
    <!-- omitted for readability -->
</util:properties>

<bean id="jpaDialect"
    class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" />

<bean id="entityManagerFactory"
    class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
    p:packagesToScan="com.example.model.core"
    p:jpaVendorAdapter-ref="jpaVendorAdapter"
    p:dataSource-ref="jpaDataSource"
    p:jpaDialect-ref="jpaDialect"
    p:jpaProperties-ref="jpaProperties" />

The PlatformTracsactionManager:

<bean id="transactionManager"
    class="org.springframework.orm.jpa.JpaTransactionManager"
    p:dataSource-ref="jpaDataSource"
    p:entityManagerFactory-ref="entityManagerFactory" />

<tx:annotation-driven proxy-target-class="false" mode="proxy"
    transaction-manager="transactionManager" />

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文