You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot+JPA多数据源路由问题:DB2无表、DB1保存失败

多数据源问题修复指南

先说说你代码里的几个关键问题,导致DB2没生成表、DB1存不了数据:

  1. 同时混用JPA的LocalContainerEntityManagerFactoryBean和原生Hibernate的LocalSessionFactoryBean,还把后者设为@Primary,直接搞混了Spring Data JPA的数据源绑定
  2. 事务管理器的Bean名字和@EnableJpaRepositories里的引用对不上,事务根本没生效,保存操作等于白做
  3. Hibernate默认只会给默认数据源生成表,DB2的DDL完全没触发
  4. 数据源切换的时机可能在事务启动之后,就算切了也没用

一步步改代码

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

验证步骤

  1. 先在MySQL里创建mydemo和mydemolocal这两个空库
  2. 启动应用,去DB2的库看有没有生成student表
  3. 分别传DB1和DB2调用saveStudent,检查对应数据库里有没有数据

内容的提问来源于stack exchange,提问作者Supriya_21

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 20:03:14