Spring with MyBatis: expected single matching bean but found 2
Asked Answered
S

4

6

I've been using Spring with MyBatis and it's been working really well for a single database. I ran into difficulties when trying to add another database (see reproducible example on Github).

I'm using Spring Java configuration (i.e. not XML). Most of the examples I've seen show how to achieve this using XML.

I have two data configuration classes (A & B) like this:

@Configuration
@MapperScan("io.woolford.database.mapper")
public class DataConfigDatabaseA {

    @Bean(name="dataSourceA")
    public DataSource dataSourceA() throws SQLException {
        SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
        dataSource.setDriver(new com.mysql.jdbc.Driver());
        dataSource.setUrl("jdbc:mysql://" + dbHostA + "/" + dbDatabaseA);
        dataSource.setUsername(dbUserA);
        dataSource.setPassword(dbPasswordA);
        return dataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSourceA());
        return sessionFactory.getObject();
    }
}

Two mappers, and a service that autowires the mappers:

@Service
public class DbService {

    @Autowired
    private DbMapperA dbMapperA;

    @Autowired
    private DbMapperB dbMapperB;

    public List<Record> getDabaseARecords(){
        return dbMapperA.getDatabaseARecords();
    }

    public List<Record> getDabaseBRecords(){
        return dbMapperB.getDatabaseBRecords();
    }

}

The application won't start:

Error creating bean with name 'dataSourceInitializer': 
  Invocation of init method failed; nested exception is 
    org.springframework.beans.factory.NoUniqueBeanDefinitionException: 
      No qualifying bean of type [javax.sql.DataSource] is defined: 
        expected single matching bean but found 2: dataSourceB,dataSourceA

I've read that it's possible to use the @Qualifier annotation to disambiguate the autowiring, though I wasn't sure where to add it.

Can you see where I'm going wrong?

Schuman answered 23/3, 2016 at 17:11 Comment(9)
Can you post the complete error message ? Usually spring tells you the autowired field and the bean causing the error.Ring
@Qaulifier("name_of_bean") can be placed before or after the @Autowired annotation of the particular field you want to target I believeVioletavioletta
Thanks @Ben75. I posted the full output to: gist.github.com/alexwoolford/1f3e799deb3be32a4356Schuman
I think you are facing a problem caused by spring-boot auto-configuration strategy. I guess that the bean 'dataSourceInitializer' defined in some spring-boot starter module is expecting -by default- one single datasource bean. (I'm not a spring-boot, nor mybatis expert... so I can't help anymore)Ring
@BretC: I added @Qualifier("dataSourceA") and the expected bean error changed: "expected single matching bean but found 2: sqlSessionFactoryA,sqlSessionFactoryB". I believe it's only possible to add one qualifier to @Autowired, so perhaps I need to add a name to @Component and use that as a qualifier?Schuman
Perhaps this will help... docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/…Candlewood
I added @Primary to the DataSource and SqlSessionFactory beans in the DataConfigDatabaseA class and it magically started working! However, it doesn't work when @Primary is added to DataConfigDatabaseB instead. This is okay as a workaround, though I don't think it's a good rule-of-thumb because there doesn't seem to be a rule to determine which beans should be annotated with @Primary.Schuman
Have you tried to create the dataSourceInitializer(s) manually in the config instead of letting them be created implicitly?Candlewood
Thanks again, @FlorianSchaetz. I added this to DataConfig classes: @Bean public DataSourceInitializer dataSourceInitializer(final DataSource dataSource) { final DataSourceInitializer initializer = new DataSourceInitializer(); initializer.setDataSource(dataSource); return initializer; } It didn't work. I need to learn more about Spring to understand what's happening.Schuman
S
1

In the end, we put each mapper in its own folder:

src/main/java/io/woolford/database/mapper/a/DbMapperA.java
src/main/java/io/woolford/database/mapper/c/DbMapperB.java

We then created two DataConfig classes, one for each database. The @MapperScan annotation resolved the expected single matching bean but found 2 issue.

@Configuration
@MapperScan(value = {"io.woolford.database.mapper.a"}, sqlSessionFactoryRef="sqlSessionFactoryA")
public class DataConfigDatabaseA {

It was necessary to add the @Primary annotation to the beans in one of the DataConfig classes:

@Bean(name="dataSourceA")
@Primary
public DataSource dataSourceA() throws SQLException {
    ...
}

@Bean(name="sqlSessionFactoryA")
@Primary
public SqlSessionFactory sqlSessionFactoryA() throws Exception {
    ...
}

Thanks to everyone who helped. No doubt, there's more than one way to do this. I did try @Qualifier and @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) as recommended by @eduardlofitskyi and @GeminiKeith, but that generated some further errors.

In case it's useful, the solution that worked for us is posted here: https://github.com/alexwoolford/mybatis-spring-multiple-mysql-reproducible-example

Schuman answered 12/5, 2016 at 16:23 Comment(0)
C
2

If you want to use two data sources at same time and they are not primary and secondary, you should disable DataSourceAutoConfiguration by @EnableAutoConfiguration(excludes = {DataSourceAutoConfiguration.class}) on your application annotated by @SpringBootApplication. Afterwards, you can create your own SqlSessionFactory and bundle your own DataSource. If you also want to use DataSourceTransactionManager, you should do that too.

In this case, you haven't disabled DataSourceAutoConfiguration, so spring framework will try to @Autowired only one DataSource but got two, error occurs.

As what I've said before, you should disable DataSourceAutoConfiguration and configure it manually.

You can disable data source auto configuration as following:

@SpringBootApplication
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class YourApplication implements CommandLineRunner {
    public static void main (String... args) {
        SpringApplication.run(YourApplication.class, args);
    }
}

And if you are really want to use multiple databases at same time, I suggest you to registering proper bean manually, such as:

package xyz.cloorc.boot.mybatis;

import org.apache.commons.dbcp.BasicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Repository;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.sql.DataSource;

@Configuration
public class SimpleTest {

    private DataSource dsA;
    private DataSource dsB;

    @Bean(name = "dataSourceA")
    public DataSource getDataSourceA() {
        return dsA != null ? dsA : (dsA = new BasicDataSource());
    }

    @Bean(name = "dataSourceB")
    public DataSource getDataSourceB() {
        return dsB != null ? dsB : (dsB = new BasicDataSource());
    }

    @Bean(name = "sqlSessionFactoryA")
    public SqlSessionFactory getSqlSessionFactoryA() throws Exception {
        // set DataSource to dsA
        return new SqlSessionFactoryBean().getObject();
    }

    @Bean(name = "sqlSessionFactoryB")
    public SqlSessionFactory getSqlSessionFactoryB() throws Exception {
        // set DataSource to dsB
        return new SqlSessionFactoryBean().getObject();
    }
}

@Repository
public class SimpleDao extends SqlSessionDaoSupport {

    @Resource(name = "sqlSessionFactoryA")
    SqlSessionFactory factory;

    @PostConstruct
    public void init() {
        setSqlSessionFactory(factory);
    }

    @Override
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        super.setSqlSessionFactory(sqlSessionFactory);
    }

    public <T> T get (Object id) {
        return super.getSqlSession().selectOne("sql statement", "sql parameters");
    }
}
Corset answered 25/4, 2016 at 13:46 Comment(1)
What a helpful answer!!! Either @EnableAutoConfiguration(excludes = DataSourceAutoConfiguration.class) or @SpringBootApplication(exclude = DataSourceAutoConfiguration.class).Ebersole
S
1

In the end, we put each mapper in its own folder:

src/main/java/io/woolford/database/mapper/a/DbMapperA.java
src/main/java/io/woolford/database/mapper/c/DbMapperB.java

We then created two DataConfig classes, one for each database. The @MapperScan annotation resolved the expected single matching bean but found 2 issue.

@Configuration
@MapperScan(value = {"io.woolford.database.mapper.a"}, sqlSessionFactoryRef="sqlSessionFactoryA")
public class DataConfigDatabaseA {

It was necessary to add the @Primary annotation to the beans in one of the DataConfig classes:

@Bean(name="dataSourceA")
@Primary
public DataSource dataSourceA() throws SQLException {
    ...
}

@Bean(name="sqlSessionFactoryA")
@Primary
public SqlSessionFactory sqlSessionFactoryA() throws Exception {
    ...
}

Thanks to everyone who helped. No doubt, there's more than one way to do this. I did try @Qualifier and @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) as recommended by @eduardlofitskyi and @GeminiKeith, but that generated some further errors.

In case it's useful, the solution that worked for us is posted here: https://github.com/alexwoolford/mybatis-spring-multiple-mysql-reproducible-example

Schuman answered 12/5, 2016 at 16:23 Comment(0)
T
0

You can use @Qualifier annotation

The problem is that you have two the same type beans in Spring container. And when you try autowire beans, Spring cannot resolve which bean inject to field

The @Qualifier annotation is the main way to work with qualifiers. It can be applied alongside @Autowired or @Inject at the point of injection to specify which bean you want to be injected.

So, your DbService should look like this:

    @Service
    public class DbService {

    @Autowired
    @Qualifier("dataSourceA")
    private DbMapperA dbMapperA;

    @Autowired
    @Qualifier("dataSourceB")
    private DbMapperB dbMapperB;

    public List<Record> getDabaseARecords(){
        return dbMapperA.getDatabaseARecords();
    }

    public List<Record> getDabaseBRecords(){
        return dbMapperB.getDatabaseBRecords();
    }

}
Tensiometer answered 10/5, 2016 at 17:38 Comment(0)
C
0

I had the same issue and could not start my Spring Boot application, and by renaming the offending class and all the layers that dealt with it, strangely the application started successfully.

I have the classes UOMService, UOMServiceImpl UOMRepository and UOMRepositoryImpl. I renamed them to be UomService, UomServiceImpl, UomRepository and UomRepositoryImpl and that solved the problem!

Comatose answered 28/1, 2017 at 10:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.