Spring Boot+JPA多数据源路由问题:DB2无表、DB1保存失败
多数据源问题修复指南
先说说你代码里的几个关键问题,导致DB2没生成表、DB1存不了数据:
- 同时混用JPA的
LocalContainerEntityManagerFactoryBean和原生Hibernate的LocalSessionFactoryBean,还把后者设为@Primary,直接搞混了Spring Data JPA的数据源绑定 - 事务管理器的Bean名字和
@EnableJpaRepositories里的引用对不上,事务根本没生效,保存操作等于白做 - Hibernate默认只会给默认数据源生成表,DB2的DDL完全没触发
- 数据源切换的时机可能在事务启动之后,就算切了也没用
一步步改代码
1. 清理冲突的EntityManager配置
删掉那个dbSessionFactory()方法,你用的是Spring Data JPA,只需要JPA的EntityManagerFactory就行。同时把@EnableJpaRepositories的配置理清楚:
修改后的配置类核心部分:
@Configuration @PropertySource({"classpath:application.properties"}) @EnableTransactionManagement @EnableJpaRepositories( basePackages = "com.demo.MyDemo.repository", // 必须指定你的Repository所在包 entityManagerFactoryRef = "multiEntityManager", transactionManagerRef = "multiTransactionManager") @EntityScan("com.demo.MyDemo.model") public class DatabaseConfiguration { private final String PACKAGE_SCAN = "com.demo.MyDemo.model"; // 保留db1DataSource、db2DataSource、multiRoutingDataSource的代码,不动它们 @Bean(name = "multiEntityManager") public LocalContainerEntityManagerFactoryBean multiEntityManager() { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(multiRoutingDataSource()); em.setPackagesToScan(PACKAGE_SCAN); HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); em.setJpaVendorAdapter(vendorAdapter); // 把硬编码的Hibernate属性改一下,不要固定ddl-auto Properties jpaProperties = new Properties(); jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.MySQL5InnoDBDialect"); jpaProperties.put("hibernate.id.new_generator_mappings", false); jpaProperties.put("hibernate.jdbc.lob.non_contextual_creation", true); em.setJpaProperties(jpaProperties); return em; } // 这里改Bean名字!和上面@EnableJpaRepositories里的transactionManagerRef对应上 @Bean(name = "multiTransactionManager") public PlatformTransactionManager multiTransactionManager() { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(multiEntityManager().getObject()); return transactionManager; } // 删掉原来的dbSessionFactory()方法,完全不需要它 }
2. 让DB2生成表
Hibernate启动时只会给默认数据源跑DDL,DB2得手动触发。加个启动类,在应用启动时切换到DB2生成表:
@Component public class DDLInitializer implements ApplicationListener<ContextRefreshedEvent> { @Autowired private EntityManagerFactory entityManagerFactory; @Override public void onApplicationEvent(ContextRefreshedEvent event) { // 切换到DB2生成表 DBContextHolder.setCurrentDb(ClientNames.DB2); Session session = entityManagerFactory.unwrap(SessionFactory.class).openSession(); try { SchemaExport export = new SchemaExport(); export.setHaltOnError(true); export.create(EnumSet.of(TargetType.DATABASE), session.getSessionFactory().getMetamodel()); } finally { session.close(); DBContextHolder.clearCurrentDb(); // 切回默认数据源 } } }
同时把application.properties里的DDL配置统一一下,别乱加没用的:
spring.datasource.jdbc-url= jdbc:mysql://localhost:3306/mydemo spring.datasource.username=root spring.datasource.password=root spring.jpa.hibernate.ddl-auto=update app.datasource.db2.jdbc-url= jdbc:mysql://localhost:3306/mydemolocal app.datasource.db2.username=root app.datasource.db2.password=root
3. 确保数据源切换在事务之前
如果你的saveStudent方法加了@Transactional,那事务会在方法调用前就启动,此时数据源已经绑定成DB1了,后面再切也白搭。解决办法:
- 要么把
@Transactional去掉, - 要么先切换数据源,再调用带事务的保存方法,
- 或者在Service里先切数据源再执行保存,最后清除上下文:
修改Service方法:
@Override public StudentEntity saveStudent(StudentEntity studentEntity, String db) { // 先切数据源,再存 String targetDb = db.equals("DB1") ? ClientNames.DB1 : ClientNames.DB2; DBContextHolder.setCurrentDb(targetDb); try { return studentRepository.save(studentEntity); } finally { DBContextHolder.clearCurrentDb(); // 方法结束后清掉,别影响下一个请求 } }
4. 检查MultiRoutingDataSource的实现
确保你的自定义数据源路由类是对的,继承AbstractRoutingDataSource并重写方法:
public class MultiRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DBContextHolder.getCurrentDb(); } }
还有DBContextHolder要用ThreadLocal存,不然多线程下会串数据源:
public class DBContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static void setCurrentDb(String db) { contextHolder.set(db); } public static String getCurrentDb() { return contextHolder.get(); } public static void clearCurrentDb() { contextHolder.remove(); } }
验证步骤
- 先在MySQL里创建
mydemo和mydemolocal这两个空库 - 启动应用,去DB2的库看有没有生成
student表 - 分别传
DB1和DB2调用saveStudent,检查对应数据库里有没有数据
内容的提问来源于stack exchange,提问作者Supriya_21
相关产品推荐
相关产品推荐

