如何使用同一持久化单元结合EntityManager与EntityManagerFactory访问不同数据源
核心问题分析
直接通过EntityManagerFactory创建的SessionImpl是Hibernate原生会话,不受Wildfly容器的事务管理控制,因此出现事务行为异常。我们需要保持使用容器管理的TransactionScopedEntityManager,同时实现动态数据源路由。
方案一:自定义路由数据源+容器管理EntityManager(推荐)
该方案通过实现一个代理数据源,根据上下文动态切换到对应客户的数据库,同时继续使用@PersistenceContext注入容器管理的EntityManager,保证事务行为正常。
1. 实现路由数据源
创建一个DataSource实现类,通过ThreadLocal存储当前客户标识,路由到对应的实际数据源:
public class CustomerRoutingDataSource implements DataSource { // 存储当前请求的客户ID private static final ThreadLocal<String> CURRENT_CUSTOMER_ID = new ThreadLocal<>(); // 缓存客户与对应数据源的映射 private final Map<String, DataSource> CUSTOMER_DS_MAP = new ConcurrentHashMap<>(); // 设置当前客户ID,需在业务操作前调用 public static void setCurrentCustomerId(String customerId) { CURRENT_CUSTOMER_ID.set(customerId); } // 清理当前客户ID,需在操作完成后调用(建议放在finally块) public static void clearCurrentCustomerId() { CURRENT_CUSTOMER_ID.remove(); } // 注册新客户的数据源 public void addCustomerDataSource(String customerId, DataSource dataSource) { CUSTOMER_DS_MAP.put(customerId, dataSource); } // 实现DataSource核心方法,路由到对应客户的数据源 @Override public Connection getConnection() throws SQLException { String customerId = CURRENT_CUSTOMER_ID.get(); if (customerId == null || !CUSTOMER_DS_MAP.containsKey(customerId)) { throw new SQLException("未找到有效客户数据源"); } return CUSTOMER_DS_MAP.get(customerId).getConnection(); } // 实现DataSource其他方法,全部委托给目标数据源 @Override public Connection getConnection(String username, String password) throws SQLException { String customerId = CURRENT_CUSTOMER_ID.get(); if (customerId == null || !CUSTOMER_DS_MAP.containsKey(customerId)) { throw new SQLException("未找到有效客户数据源"); } return CUSTOMER_DS_MAP.get(customerId).getConnection(username, password); } // 省略其他DataSource接口方法的委托实现 }
2. 在Wildfly中配置路由数据源
修改standalone.xml(或domain.xml),注册路由数据源作为持久化单元的数据源:
<datasource jndi-name="java:jboss/datasources/CustomerRoutingDS" pool-name="CustomerRoutingDS"> <!-- 占位配置,实际由路由类接管 --> <connection-url>jdbc:h2:mem:placeholder</connection-url> <driver>h2</driver> <security> <user-name>sa</user-name> <password>sa</password> </security> <!-- 指定自定义路由数据源类 --> <connection-properties> <property name="wildfly.jdbc.data-source.class" value="com.yourpackage.CustomerRoutingDataSource"/> </connection-properties> </datasource>
3. 修改持久化单元配置
在persistence.xml中,将持久化单元的数据源指向路由数据源:
<persistence-unit name="YOUR_PU_NAME" transaction-type="JTA"> <jta-data-source>java:jboss/datasources/CustomerRoutingDS</jta-data-source> <!-- 保留原有实体映射、Hibernate配置等 --> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/> <!-- 若使用缓存,需配置客户隔离的缓存策略,见下文补充 --> </properties> </persistence-unit>
4. 业务代码中使用动态数据源
继续使用@PersistenceContext注入容器管理的EntityManager,在业务操作前设置当前客户ID:
@Stateless public class CustomerBusinessService { @PersistenceContext(unitName = "YOUR_PU_NAME") private EntityManager em; public void saveCustomerData(String customerId, YourEntity entity) { try { // 设置当前客户ID,路由到对应数据源 CustomerRoutingDataSource.setCurrentCustomerId(customerId); // 容器管理的EntityManager,事务行为正常 em.persist(entity); } finally { // 清理上下文,避免线程污染 CustomerRoutingDataSource.clearCurrentCustomerId(); } } }
5. 动态注册客户数据源
对于新增的客户,可以通过Wildfly管理API(或CLI)动态创建数据源,再注册到路由数据源中:
@Inject private InitialContext initialContext; public void registerNewCustomer(String customerId, String dsJndiName) throws NamingException { DataSource customerDs = (DataSource) initialContext.lookup(dsJndiName); // 获取路由数据源实例(可通过JNDI lookup或单例模式) CustomerRoutingDataSource routingDs = (CustomerRoutingDataSource) initialContext.lookup("java:jboss/datasources/CustomerRoutingDS"); routingDs.addCustomerDataSource(customerId, customerDs); }
通过Wildfly CLI创建新数据源示例:
/subsystem=datasources/data-source=customer_001DS:add( jndi-name=java:jboss/datasources/customer_001DS, driver-name=postgresql, connection-url=jdbc:postgresql://localhost:5432/customer_001, user-name=db_user, password=db_pass )
补充:Hibernate二级缓存隔离
若使用Hibernate二级缓存,需避免不同客户的数据混淆,可通过自定义缓存区域解析器实现客户隔离:
public class CustomerIsolatedCacheResolver implements CacheRegionResolver { private final CacheRegionResolver delegateResolver; public CustomerIsolatedCacheResolver(CacheRegionResolver delegateResolver) { this.delegateResolver = delegateResolver; } @Override public Region resolveRegion(String regionName) { // 给缓存区域添加客户ID前缀 String customerId = CustomerRoutingDataSource.CURRENT_CUSTOMER_ID.get(); if (customerId != null) { regionName = customerId + "_" + regionName; } return delegateResolver.resolveRegion(regionName); } @Override public Region resolveQueryRegion(String regionName) { String customerId = CustomerRoutingDataSource.CURRENT_CUSTOMER_ID.get(); if (customerId != null) { regionName = customerId + "_" + regionName; } return delegateResolver.resolveQueryRegion(regionName); } }
在persistence.xml中配置该解析器:
<property name="hibernate.cache.region_resolver" value="com.yourpackage.CustomerIsolatedCacheResolver"/>
方案二:CDI扩展实现事务范围EntityManager(备选)
若无法使用路由数据源,可通过CDI扩展拦截EntityManager的创建,动态绑定数据源并包装为事务范围的代理。但该方案需依赖Wildfly内部API,兼容性较差,不推荐生产环境使用。
内容的提问来源于stack exchange,提问作者Faliorn

