web-dev-qa-db-fra.com

Spring Boot, Spring Data JPA avec plusieurs sources de données

J'essaie de connecter chaque @Repositories à différentes sources de données avec Spring Boot et Spring Data JPA. J'ai utilisé ce qui suit, http://xantorohara.blogspot.com/2013/11/spring-boot-jdbc-with-multiple.html , comme référence. Voici le code que j'utilise pour implémenter une solution similaire à l'aide de Spring Data JPA.

CustomerDbConfig.Java (première connexion à la source de données)

@Configuration
@EnableJpaRepositories(
        entityManagerFactoryRef = "orderEntityManager",
        transactionManagerRef = "orderTransactionManager",
        basePackages = {"com.mm.repository.customer"})
public class CustomerDbConfig {

    @Bean(name = "customerEntityManager")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(){
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource());
        em.setPackagesToScan(new String[] {"com.mm.domain.customer"});

        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(additionalJpaProperties());
        em.setPersistenceUnitName("customerPersistence");
        em.setPackagesToScan("com.mm.domain.customer");

        return em;
    }

    Properties additionalJpaProperties(){
        Properties properties = new Properties();
        properties.setProperty("hibernate.hbm2ddl.auto", "create-drop");
        properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        properties.setProperty("hibernate.show_sql", "true");

        return properties;
    }

    @Bean
    public DataSource dataSource(){
        return DataSourceBuilder.create()
                .url("jdbc:h2:mem:customer:H2")
                .driverClassName("org.h2.Driver")
                .username("sa")
                .password("")
                .build();
    }   

    @Bean(name = "customerTransactionManager")
    public PlatformTransactionManager transactionManager(EntityManagerFactory emf){
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(emf);

        return transactionManager;
    }
}

CustomerDbConfig.Java (deuxième source de données)

@Configuration
@EnableJpaRepositories(
        entityManagerFactoryRef = "orderEntityManager",
        transactionManagerRef = "orderTransactionManager",
        basePackages = {"com.mm.repository.customer"})
public class CustomerDbConfig {

    @Bean(name = "customerEntityManager")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(){
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource());
        em.setPackagesToScan(new String[] {"com.mm.domain.customer"});

        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(additionalJpaProperties());
        em.setPersistenceUnitName("customerPersistence");
        em.setPackagesToScan("com.mm.domain.customer");

        return em;
    }

    Properties additionalJpaProperties(){
        Properties properties = new Properties();
        properties.setProperty("hibernate.hbm2ddl.auto", "create-drop");
        properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        properties.setProperty("hibernate.show_sql", "true");

        return properties;
    }

    @Bean
    public DataSource dataSource(){
        return DataSourceBuilder.create()
                .url("jdbc:h2:mem:customer:H2")
                .driverClassName("org.h2.Driver")
                .username("sa")
                .password("")
                .build();
    }   

    @Bean(name = "customerTransactionManager")
    public PlatformTransactionManager transactionManager(EntityManagerFactory emf){
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(emf);

        return transactionManager;
    }
}

Customer.Java (modèle)

@Entity
@Table(name = "customer")
@Data
@EqualsAndHashCode(exclude = {"id"})
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "name", nullable = false)
    private String name;

    @Column(name = "age", nullable = false)
    private Integer age;

....

Order.Java (modèle)

@Entity
@Table(name = "order")
@Data
@EqualsAndHashCode(exclude = {"id"})
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "code", nullable = false)
    private Integer code;

    @Column(name = "quality", nullable = false)
    private Integer quality;

...

CustomerRepository.Java

public interface CustomerRepository extends JpaRepository<Customer, Integer>{

}

OrderRepository.Java

public interface OrderRepository extends JpaRepository<Order, Integer> {

}

Enfin, Application.Java

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application extends SpringApplication{

       public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }

        @Bean
        public ServletRegistrationBean h2Console() {
            ServletRegistrationBean reg = new ServletRegistrationBean(new WebServlet(), "/console/*");
            reg.setLoadOnStartup(1);
            return reg;
        }
}

Pendant le démarrage, les exceptions suivants sont levés:

-10-10 15:45:24.757 ERROR 1549 --- [           main] o.s.boot.SpringApplication               : Application startup failed

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'customerTransactionManager' defined in class path resource [com/mm/boot/multidb/CustomerConfig.class]: Unsatisfied dependency expressed through constructor argument with index 0 of type [javax.persistence.EntityManagerFactory]: : No qualifying bean of type [javax.persistence.EntityManagerFactory] is defined: expected single matching bean but found 2: customerEntityManager,orderEntityManager; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [javax.persistence.EntityManagerFactory] is defined: expected single matching bean but found 2: customerEntityManager,orderEntityManager
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.Java:747)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.Java:462)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.Java:1095)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.Java:990)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.Java:504)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.Java:475)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.Java:302)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.Java:228)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.Java:298)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.Java:193)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.Java:706)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.Java:762)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.Java:482)
    at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.Java:109)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.Java:691)
    at org.springframework.boot.SpringApplication.run(SpringApplication.Java:320)
    at org.springframework.boot.SpringApplication.run(SpringApplication.Java:952)
    at org.springframework.boot.SpringApplication.run(SpringApplication.Java:941)
    at com.mm.boot.multidb.Application.main(Application.Java:17)
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [javax.persistence.EntityManagerFactory] is defined: expected single matching bean but found 2: customerEntityManager,orderEntityManager
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.Java:974)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.Java:862)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.Java:811)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.Java:739)
    ... 18 common frames omitted

Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'customerTransactionManager' defined in class path resource [com/mm/boot/multidb/CustomerConfig.class]: Unsatisfied dependency expressed through constructor argument with index 0 of type [javax.persistence.EntityManagerFactory]: : No qualifying bean of type [javax.persistence.EntityManagerFactory] is defined: expected single matching bean but found 2: customerEntityManager,orderEntityManager; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [javax.persistence.EntityManagerFactory] is defined: expected single matching bean but found 2: customerEntityManager,orderEntityManager
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.Java:747)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.Java:462)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.Java:1095)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.Java:990)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.Java:504)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.Java:475)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.Java:302)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.Java:228)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.Java:298)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.Java:193)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.Java:706)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.Java:762)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.Java:482)
    at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.Java:109)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.Java:691)
    at org.springframework.boot.SpringApplication.run(SpringApplication.Java:320)
    at org.springframework.boot.SpringApplication.run(SpringApplication.Java:952)
    at org.springframework.boot.SpringApplication.run(SpringApplication.Java:941)
    at com.mm.boot.multidb.Application.main(Application.Java:17)
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [javax.persistence.EntityManagerFactory] is defined: expected single matching bean but found 2: customerEntityManager,orderEntityManager
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.Java:974)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.Java:862)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.Java:811)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.Java:739)
    ... 18 more

Le code complet de l'exemple est disponible sur GitHub ( https://github.com/tonym2105/samples/tree/master/boot-multidb-sample )

Merci d'avance pour votre aide.

31
tonym2105

Il existe un autre moyen d'avoir plusieurs sources de données en utilisant @EnableAutoConfiguration et application.properties.

En gros, placez plusieurs informations de configuration dataSource sur application.properties et générez la configuration par défaut (dataSource et entityManagerFactory) automatiquement pour le premier dataSource avec @EnableAutoConfiguration. Mais pour DataSource suivant, créez manuellement DataSource, entityManagerFactory et transactionManager à l'aide du fichier de propriétés.

Voici l'exemple de la configuration de deux sources de données . La première source de données est configurée par @EnableAutoConfiguration, qui ne peut être affectée que pour une configuration et non pour plusieurs. Et cela va générer 'transactionManager' par DataSourceTransactionManager , qui ressemble à TransactionManager par défaut généré par l'annotation. Cependant, j’ai vu que la transaction ne commençait pas d’émission sur le thread à partir du pool de threads planifié uniquement pour le DataSourceTransactionManager par défaut et également s’il existait plusieurs gestionnaires de transaction. Je crée donc transactionManager manuellement par JpaTransactionManager également pour le premier dataSource avec affectation du nom du bean 'transactionManager' et du entityManagerFactory par défaut. JpaTransactionManager pour la première source de données résout sûrement le problème de transaction étrange sur le thread de ScheduledThreadPool.

Mise à jour pour Spring Boot 1.3.0.RELEASE

J'ai trouvé ma configuration précédente avec @EnableAutoConfiguration pour la source de données par défaut qui pose problème pour la recherche de entityManagerFactory avec la version Spring Boot 1.3. Peut-être que default entityManagerFactory n'est pas généré par @EnableAutoConfiguration, une fois après avoir présenté mon propre transactionManager ..__, je crée maintenant entityManagerFactory par moi-même. Je n'ai donc pas besoin d'utiliser @EntityScan. Donc, il semble que la configuration de @EnableAutoConfiguration soit de plus en plus utilisée.

La deuxième source de données est configurée sans @EnableAutoConfiguration et crée 'anotherTransactionManager' manuellement.

Puisqu'il existe plusieurs extensions transactionManager à partir de PlatformTransactionManager, nous devons spécifier quel transactionManager utiliser sur chaque annotation @Transactional.

Configuration du référentiel par défaut

@Configuration
@EnableTransactionManagement
@EnableAutoConfiguration
@EnableJpaRepositories(
        entityManagerFactoryRef = "entityManagerFactory",
        transactionManagerRef = "transactionManager",
        basePackages = {"com.mysource.repository"})
public class RepositoryConfig {
    @Autowired
    JpaVendorAdapter jpaVendorAdapter;

    @Autowired
    DataSource dataSource;

    @Bean(name = "entityManager")
    public EntityManager entityManager() {
        return entityManagerFactory().createEntityManager();
    }

    @Primary
    @Bean(name = "entityManagerFactory")
    public EntityManagerFactory entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(dataSource);
        emf.setJpaVendorAdapter(jpaVendorAdapter);
        emf.setPackagesToScan("com.mysource.model");
        emf.setPersistenceUnitName("default");   // <- giving 'default' as name
        emf.afterPropertiesSet();
        return emf.getObject();
    }

    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(entityManagerFactory());
        return tm;
    }
}

Une autre configuration de référentiel

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef = "anotherEntityManagerFactory",
        transactionManagerRef = "anotherTransactionManager",
        basePackages = {"com.mysource.anothersource.repository"})
public class AnotherRepositoryConfig {
    @Autowired
    JpaVendorAdapter jpaVendorAdapter;

    @Value("${another.datasource.url}")
    private String databaseUrl;

    @Value("${another.datasource.username}")
    private String username;

    @Value("${another.datasource.password}")
    private String password;

    @Value("${another.dataource.driverClassName}")
    private String driverClassName;

    @Value("${another.datasource.hibernate.dialect}")
    private String dialect;

    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource(databaseUrl, username, password);
        dataSource.setDriverClassName(driverClassName);
        return dataSource;
    }

    @Bean(name = "anotherEntityManager")
    public EntityManager entityManager() {
        return entityManagerFactory().createEntityManager();
    }

    @Bean(name = "anotherEntityManagerFactory")
    public EntityManagerFactory entityManagerFactory() {
        Properties properties = new Properties();
        properties.setProperty("hibernate.dialect", dialect);

        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(dataSource());
        emf.setJpaVendorAdapter(jpaVendorAdapter);
        emf.setPackagesToScan("com.mysource.anothersource.model");   // <- package for entities
        emf.setPersistenceUnitName("anotherPersistenceUnit");
        emf.setJpaProperties(properties);
        emf.afterPropertiesSet();
        return emf.getObject();
    }

    @Bean(name = "anotherTransactionManager")
    public PlatformTransactionManager transactionManager() {
        return new JpaTransactionManager(entityManagerFactory());
    }
}

application.properties

# database configuration
spring.datasource.url=jdbc:h2:file:~/main-source;AUTO_SERVER=TRUE
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.continueOnError=true
spring.datasource.initialize=false

# another database configuration
another.datasource.url=jdbc:sqlserver://localhost:1433;DatabaseName=another;
another.datasource.username=username
another.datasource.password=
another.datasource.hibernate.dialect=org.hibernate.dialect.SQLServer2008Dialect 
another.datasource.driverClassName=com.Microsoft.sqlserver.jdbc.SQLServerDriver

Choisissez le transactionManager approprié pour l'annotation @Transactional

Service pour la première source de données

@Service("mainService")
@Transactional("transactionManager")
public class DefaultDataSourceServiceImpl implements DefaultDataSourceService       
{

   //

}

Service pour une autre source de données

@Service("anotherService")
@Transactional("anotherTransactionManager")
public class AnotherDataSourceServiceImpl implements AnotherDataSourceService 
{

   //

}
36
Steve Park

voici ma solution. base sur spring-boot.1.2.5.RELEASE.

application.properties

first.datasource.driver-class-name=com.mysql.jdbc.Driver
first.datasource.url=jdbc:mysql://127.0.0.1:3306/test
first.datasource.username=
first.datasource.password=
first.datasource.validation-query=select 1

second.datasource.driver-class-name=com.mysql.jdbc.Driver
second.datasource.url=jdbc:mysql://127.0.0.1:3306/test2
second.datasource.username=
second.datasource.password=
second.datasource.validation-query=select 1

DataSourceConfig.Java

@Configuration
public class DataSourceConfig {
    @Bean
    @Primary
    @ConfigurationProperties(prefix="first.datasource")
    public DataSource firstDataSource() {
        DataSource ds =  DataSourceBuilder.create().build();
        return ds;
    }

    @Bean
    @ConfigurationProperties(prefix="second.datasource")
    public DataSource secondDataSource() {
        DataSource ds =  DataSourceBuilder.create().build();
        return ds;
    }
}
6
david

J'ai vérifié le code source que vous avez fourni sur GitHub. Il y avait plusieurs erreurs/fautes de frappe dans la configuration.

Dans CustomerDbConfig/OrderDbConfig, vous devez faire référence à customerEntityManager et les packages doivent pointer vers les packages existants:

@Configuration
@EnableJpaRepositories(
    entityManagerFactoryRef = "customerEntityManager",
    transactionManagerRef = "customerTransactionManager",
    basePackages = {"com.mm.boot.multidb.repository.customer"})
public class CustomerDbConfig {

Les packages à analyser dans customerEntityManager et orderEntityManager ne pointaient pas tous deux sur le package approprié:

em.setPackagesToScan("com.mm.boot.multidb.model.customer");

De même, l'injection de EntityManagerFactory appropriée n'a pas fonctionné. CA devrait etre:

@Bean(name = "customerTransactionManager")
public PlatformTransactionManager transactionManager(EntityManagerFactory customerEntityManager){

}

Ce qui précède était à l'origine du problème et de l'exception. Tout en fournissant le nom dans une méthode @Bean, vous êtes sûr d’avoir reçu la bonne EMF injectée.

La dernière chose que j'ai faite a été de désactiver la configuration automatique de JpaRepositories:

@EnableAutoConfiguration(exclude = JpaRepositoriesAutoConfiguration.class)

Et avec tous les correctifs, l'application démarre comme vous vous en doutez probablement!

2
Rafal Borowiec

grâce aux réponses de Steve Park et Rafal Borowiec, mon code a fonctionné. Cependant, j’avais un problème: le DriverManagerDataSource est une "simple" implémentation et ne vous donne PAS un ConnectionPool (check http: // docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/datasource/DriverManagerDataSource.html ).

Par conséquent, j'ai remplacé les fonctions qui retournent la DataSource pour la secondDB à.

public DataSource <secondaryDB>DataSource() {
    // use DataSourceBuilder and NOT DriverManagerDataSource 
    // as this would NOT give you ConnectionPool
    DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
    dataSourceBuilder.url(databaseUrl);
    dataSourceBuilder.username(username);
    dataSourceBuilder.password(password);
    dataSourceBuilder.driverClassName(driverClassName);
    return dataSourceBuilder.build();
}

De même, si vous n'avez pas besoin de la variable EntityManager en tant que telle, vous pouvez supprimer les annotations entityManager() et @Bean

De plus, vous voudrez peut-être supprimer l'annotation basePackages de votre classe de configuration: le maintenir avec l'appel factoryBean.setPackagesToScan() suffit.

1
codemonkey