← Назад к вопросам

Как в Spring Boot задать БД по умолчанию при наличии нескольких DataSource

2.3 Middle🔥 151 комментариев
#Spring Boot и Spring Data

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Несколько DataSource в Spring Boot и выбор основного

Этот сценарий встречается, когда приложение работает с несколькими базами данных. Spring нужно явно сказать, какая из них основная.

1. Проблема: Ambiguous DataSource

По умолчанию Spring Boot создаёт один DataSource из application.properties/yml. Но если их несколько:

# application.yml
spring:
  datasource:
    primary:
      url: jdbc:postgresql://localhost:5432/app_db
      username: app_user
      password: secret
      driver-class-name: org.postgresql.Driver
    
    replica:
      url: jdbc:postgresql://replica.db:5432/app_db
      username: app_user
      password: secret
      driver-class-name: org.postgresql.Driver
    
    analytics:
      url: jdbc:mysql://analytics.db:3306/analytics
      username: analytics_user
      password: secret
      driver-class-name: com.mysql.cj.jdbc.Driver

Спринг не знает, какой использовать:

Error: NoUniqueBeanDefinitionException: 
No qualifying bean of type 'javax.sql.DataSource' is available: 
expected single matching bean but found 3: primaryDataSource, replicaDataSource, analyticsDataSource

2. Решение 1: @Primary аннотация

Простой и рекомендуемый подход — отметить основной DataSource:

@Configuration
public class DataSourceConfiguration {
    
    // Основная БД
    @Primary // ← ЭТО ГЛАВНОЕ!
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    // Replica — only for read
    @Bean(name = "replicaDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.replica")
    public DataSource replicaDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    // Analytics — отдельная БД
    @Bean(name = "analyticsDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.analytics")
    public DataSource analyticsDataSource() {
        return DataSourceBuilder.create().build();
    }
}

Теперь Spring знает какой использовать по умолчанию:

@Repository
public class UserRepository {
    
    @Autowired
    private DataSource dataSource; // Получит primaryDataSource (благодаря @Primary)
    
    public User findById(UUID id) {
        // Используется основная БД
        return new JdbcTemplate(dataSource).queryForObject(
            "SELECT * FROM users WHERE id = ?",
            new UserRowMapper(),
            id
        );
    }
}

3. Решение 2: Явное указание через @Qualifier

Если нужно выбрать конкретный DataSource:

@Service
public class AnalyticsService {
    
    // Явно указываем, какой DataSource использовать
    @Autowired
    @Qualifier("analyticsDataSource")
    private DataSource analyticsDataSource;
    
    public void saveMetric(String metric, long value) {
        JdbcTemplate template = new JdbcTemplate(analyticsDataSource);
        template.update(
            "INSERT INTO metrics (name, value) VALUES (?, ?)",
            metric, value
        );
    }
}

@Service
public class UserService {
    
    // Явно указываем primary
    @Autowired
    @Qualifier("primaryDataSource")
    private DataSource dataSource;
    
    public void createUser(User user) {
        // Используется только основная БД
        userRepository.save(user);
    }
}

4. Решение 3: Несколько EntityManagers (JPA)

Для JPA нужен более сложный setup:

@Configuration
@EnableTransactionManagement
public class JpaConfiguration {
    
    // Основная БД
    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
            @Qualifier("primaryDataSource") DataSource dataSource) {
        
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.example.domain");
        
        JpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(adapter);
        
        Properties props = new Properties();
        props.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
        props.setProperty("hibernate.hbm2ddl.auto", "validate");
        em.setJpaProperties(props);
        
        return em;
    }
    
    @Primary
    @Bean
    public PlatformTransactionManager primaryTransactionManager(
            @Qualifier("primaryEntityManagerFactory") EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
    
    // ===== Replica DataSource =====
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.replica")
    public DataSource replicaDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    public LocalContainerEntityManagerFactoryBean replicaEntityManagerFactory(
            @Qualifier("replicaDataSource") DataSource dataSource) {
        
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.example.domain");
        
        JpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(adapter);
        
        return em;
    }
    
    @Bean
    public PlatformTransactionManager replicaTransactionManager(
            @Qualifier("replicaEntityManagerFactory") EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}

Ломбардирование с несколькими EntityManagers требует использования разных пакетов для сущностей:

@Transactional("primaryTransactionManager")
@Repository
public class UserRepository extends JpaRepository<User, UUID> {
    // Использует primaryEntityManagerFactory
}

@Transactional("replicaTransactionManager")
@Repository
public class UserReplicaRepository extends JpaRepository<User, UUID> {
    // Использует replicaEntityManagerFactory (read-only)
}

5. Практический пример: Routing

Часто нужно маршрутизировать запросы:

@Component
public class DataSourceRouter extends AbstractRoutingDataSource {
    
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    
    public static void setDataSourceKey(String key) {
        contextHolder.set(key);
    }
    
    public static void clearDataSourceKey() {
        contextHolder.remove();
    }
    
    @Override
    protected Object determineCurrentLookupKey() {
        return contextHolder.get();
    }
}

@Configuration
public class DataSourceConfiguration {
    
    @Bean
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:postgresql://localhost:5432/primary")
            .username("user")
            .password("pass")
            .build();
    }
    
    @Bean
    public DataSource replicaDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:postgresql://replica:5432/primary")
            .username("user")
            .password("pass")
            .build();
    }
    
    @Bean
    @Primary
    public DataSource routingDataSource(
            @Qualifier("primaryDataSource") DataSource primary,
            @Qualifier("replicaDataSource") DataSource replica) {
        
        DataSourceRouter router = new DataSourceRouter();
        Map<Object, Object> sources = new HashMap<>();
        sources.put("primary", primary);
        sources.put("replica", replica);
        
        router.setTargetDataSources(sources);
        router.setDefaultTargetDataSource(primary);
        
        return router;
    }
}

// Использование
@Service
public class UserService {
    
    @Autowired
    private UserRepository repository;
    
    // Чтение — используем replica
    @Transactional(readOnly = true)
    public User findById(UUID id) {
        DataSourceRouter.setDataSourceKey("replica");
        try {
            return repository.findById(id).orElse(null);
        } finally {
            DataSourceRouter.clearDataSourceKey();
        }
    }
    
    // Запись — используем primary
    @Transactional
    public void save(User user) {
        DataSourceRouter.setDataSourceKey("primary");
        try {
            repository.save(user);
        } finally {
            DataSourceRouter.clearDataSourceKey();
        }
    }
}

6. Best Practices

// ✅ ПРАВИЛЬНО
@Configuration
public class DataSourceConfig {
    
    @Primary // Явно отмечаем основную
    @Bean
    public DataSource primaryDataSource() { }
    
    @Bean
    public DataSource secondaryDataSource() { }
}

// ❌ ПЛОХО — неясно, какая основная
@Configuration
public class DataSourceConfig {
    
    @Bean
    public DataSource dataSource1() { }
    
    @Bean
    public DataSource dataSource2() { }
}

// ✅ ПРАВИЛЬНО — явно указываем
@Service
public class MyService {
    
    @Autowired
    @Qualifier("analyticsDataSource")
    private DataSource analyticsDs;
}

// ❌ ПЛОХО — неясно, какой DataSource внедрится
@Service
public class MyService {
    
    @Autowired
    private DataSource dataSource;
}

Выводы

  1. @Primary — указывает основной DataSource для автоматического внедрения
  2. @Qualifier — явно указывает, какой конкретный DataSource использовать
  3. AbstractRoutingDataSource — динамическая маршрутизация между БД
  4. Несколько EntityManagers — нужны для JPA с разными БД
  5. ThreadLocal — важен для правильной маршрутизации в многопоточном окружении

Обычно используется комбинация: @Primary для основного, @Qualifier для остальных.

Как в Spring Boot задать БД по умолчанию при наличии нескольких DataSource | PrepBro