在spring boot应用程序中管理多个数据源是一项常见的需求,尤其是在需要与不同业务领域或遗留系统交互的复杂场景中。jpa(java persistence api)通过hibernate等实现,为对象关系映射提供了强大的支持。然而,当涉及到多个数据源,并且需要执行原生sql查询时,可能会遇到一些挑战,例如查询错误地指向了错误的数据源。
问题描述假设一个Spring Boot应用配置了两个PostgreSQL数据库:powwow(主数据库)和pims(次要数据库)。应用程序能够成功地连接到这两个数据库,并独立地执行事务。对于次要数据库pims,通过JPA Repository执行非原生查询时一切正常。但是,当尝试在pims数据库上执行原生SQL查询(例如查询merchants表)时,却收到PSQLException: ERROR: relation "merchants" does not exist的错误。进一步的测试表明,相同的原生查询如果针对powwow数据库中的表,则可以正常执行。这表明问题出在次要数据源pims的配置上,导致原生查询没有正确地路由到pims数据库。
问题的核心在于,当存在多个EntityManagerFactory实例时,Spring需要明确知道@PersistenceContext注解应该注入哪一个EntityManager。如果未明确指定,Spring可能会选择默认的或主EntityManager,导致原生查询在错误的数据库上下文中执行。
解决方案:使用 PersistenceUnitName解决此问题的关键在于为每个EntityManagerFactory明确指定一个唯一的PersistenceUnitName,并在需要注入特定EntityManager的地方通过@PersistenceContext注解引用这个名称。
PersistenceUnitName是一个逻辑名称,用于标识一个持久化单元,它封装了一组实体类及其到特定数据源的映射。通过为每个数据源的EntityManagerFactory设置一个独特的PersistenceUnitName,并让DAO层在注入EntityManager时明确指定所需的PersistenceUnitName,可以消除Spring在选择EntityManager时的歧义。
配置次要数据源的持久化单元名称首先,在次要数据源(例如pims)的JPA配置类中,为LocalContainerEntityManagerFactoryBean设置一个唯一的PersistenceUnitName。
PersistencePimsAutoConfiguration.java 更新示例:
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.HashMap; @Configuration @PropertySource({"classpath:application.properties"}) @EnableJpaRepositories( basePackages = {"com.xxxx.powwow.dao.pims", "com.xxxx.powwow.repositories.pims"}, entityManagerFactoryRef = "pimsEntityManager", transactionManagerRef = "pimsTransactionManager") public class PersistencePimsAutoConfiguration { private Logger logger = LogManager.getLogger(PersistencePimsAutoConfiguration.class); @Value("${spring.datasource1.jdbc-url}") private String url; @Value("${spring.datasource1.username}") private String username; @Value("${spring.jpa.hibernate.ddl-auto}") private String hbm2ddl; @Value("${spring.jpa.database-platform}") private String platform; @Value("${spring.jpa.properties.hibernate.dialect}") private String dialect; @Value("${spring.profiles.active}") private String profile; @Bean @ConfigurationProperties(prefix="spring.datasource1") public DataSource pimsDataSource() { return DataSourceBuilder.create().build(); } @Bean public LocalContainerEntityManagerFactoryBean pimsEntityManager() { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(pimsDataSource()); em.setPackagesToScan(new String[] {"com.xxxx.powwow.entities.pims"}); HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); em.setJpaVendorAdapter(vendorAdapter); HashMap<String, Object> properties = new HashMap<>(); properties.put("hibernate.hbm2ddl.auto", hbm2ddl); properties.put("hibernate.dialect", dialect); em.setJpaPropertyMap(properties); // **新增行:设置持久化单元名称** em.setPersistenceUnitName("pimsPersistenceUnit"); // 确保此名称在整个应用中唯一 String host = null; try { host = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { throw new RuntimeException(e); } logger.info("Setting spring.datasource1 (pims): hibernate.hbm2ddl.auto='"+hbm2ddl+"', platform='"+platform+"', url='"+url+"', username='"+username+"', host='"+host+"', profile='"+profile+"'."); return em; } @Bean public PlatformTransactionManager pimsTransactionManager() { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(pimsEntityManager().getObject()); return transactionManager; } }在DAO层引用指定的持久化单元
接着,在需要使用次要数据源的EntityManager的DAO类中,通过@PersistenceContext注解的unitName属性引用上一步设置的持久化单元名称。
BookingHistoryReportDao.java 更新示例:
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; import java.util.Date; import java.util.List; @Component @Transactional("pimsTransactionManager") public class BookingHistoryReportDao { private Logger logger = LogManager.getLogger(BookingHistoryReportDao.class); // **修改行:指定持久化单元名称** @PersistenceContext(unitName = "pimsPersistenceUnit") private EntityManager entityManager; public void executeBookingHistoryReport(Date startDate, Date endDate, List<Integer> companyIds) { final String sql = getSQLBookingHistoryReportDao(); try { Query qry = entityManager.createNativeQuery(sql); List<String> merchants = qry.getResultList(); logger.info("done"); } catch (Exception e) { logger.error("Error executing query for BookingHistoryReport.", e); logger.info(sql); } } private String getSQLBookingHistoryReportDao() { return "select company_name from Merchants limit 100"; } }工作原理
当Spring容器启动并扫描到多个LocalContainerEntityManagerFactoryBean(即多个EntityManagerFactory)时,它需要一种机制来区分它们。
- em.setPersistenceUnitName("pimsPersistenceUnit"): 这行代码为pimsEntityManager这个EntityManagerFactory实例赋予了一个逻辑名称pimsPersistenceUnit。这个名称在Spring的内部注册表中是唯一的,用于标识这个特定的持久化单元。
- @PersistenceContext(unitName = "pimsPersistenceUnit"): 当Spring尝试将EntityManager注入到BookingHistoryReportDao时,unitName属性明确告诉Spring,它应该注入与名为pimsPersistenceUnit的持久化单元相关联的EntityManager。这样,entityManager实例就保证了与pims数据库的连接和事务上下文相关联。
- 原生查询的正确路由: EntityManager.createNativeQuery()方法会使用其所属持久化单元的底层数据库连接来执行SQL。通过明确指定unitName,确保了entityManager实例指向pims数据库,因此原生查询select company_name from Merchants将会在pims数据库上执行,而不是错误地路由到powwow数据库。
- 唯一性: 确保每个LocalContainerEntityManagerFactoryBean的PersistenceUnitName都是唯一的。
- 一致性: 对于每个数据源,其相关的@EnableJpaRepositories注解应通过entityManagerFactoryRef和transactionManagerRef明确引用对应的EntityManagerFactory和PlatformTransactionManager Bean。
- 主数据源: 如果存在一个“主”数据源,并且其EntityManagerFactory没有明确设置PersistenceUnitName,或者在@PersistenceContext中没有指定unitName,Spring可能会将其视为默认的EntityManager。因此,即使是主数据源,也建议为其设置PersistenceUnitName以避免潜在的歧义。
- 事务管理: 确保JpaTransactionManager正确地关联到其对应的EntityManagerFactory。本例中pimsTransactionManager通过transactionManager.setEntityManagerFactory(pimsEntityManager().getObject())正确地完成了关联。
- 实体包扫描: em.setPackagesToScan()应指向该数据源对应的实体类包,确保实体与正确的数据库映射。
在Spring Boot应用中配置多个JPA数据源时,尤其是涉及到原生SQL查询时,明确的配置至关重要。通过为每个LocalContainerEntityManagerFactoryBean设置唯一的PersistenceUnitName,并在DAO层通过@PersistenceContext(unitName = "...")引用它,可以有效地解决原生查询指向错误数据库的问题,确保应用程序的稳定性和正确性。这种显式配置的方法增强了代码的可读性和可维护性,是多数据源JPA配置中的一个重要实践。
以上就是Spring Boot多数据库JPA原生查询配置指南的详细内容,更多请关注知识资源分享宝库其它相关文章!
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。