Spring 4, MyBatis, multiple data sources with annotations - java

I am currently in a Spring 4 application that uses MyBatis and is completely annotation-driven (that cannot change per architecture requirements). I am trying to add a second data source definition with a completely separate set of mapping configurations.
The problem I am having is that I cannot get the two data sources to play nicely together.
I created a new, virtually identical class and added #Qualifier data to the new file.
The configuration for the classes looks like this:
Data Source 1
#Configuration
#MapperScan (basePackages = "com.myproject.package1", annotationClass = Mapper.class)
public class DataSource1 {
#Bean
#Qualifier ("DS1")
public DataSource getDataSource() {
/* configuration loaded */
}
#Bean
#Qualifier ("DS1")
public SqlSessionFactory getSqlSessionFactory() {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(getDataSource());
/* mapper resources added */
return bean.getObject();
}
}
Data Source 2
#Configuration
#MapperScan (basePackages = "com.myproject.package2", annotationClass = Mapper.class)
public class DataSource2 {
#Bean
#Qualifier ("DS2")
public DataSource getDataSource() {
/* configuration loaded */
}
#Bean
#Qualifier ("DS2")
public SqlSessionFactory getSqlSessionFactory() {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(getDataSource());
/* mapper resources added */
return bean.getObject();
}
}
When this runs I get exception messages like:
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)
If I comment-out the data in DS2, DS1 works just fine again. I tried adding the mapper scanning configuration data in another bean and setting the name of the SqlSessionFactoryBean to pass into it but that did not work.
Suggestions?
UPDATE
I looked at this post and updated to use the following.
#Bean (name = "the_factory_1")
public SqlSessionFactory getSqlSessionFactory() { /* same code */ }
#Bean
public MapperScannerConfigurer getMapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setBasePackage("com.myproject.package1");
configurer.setAnnotationClass(Mapper.class);
configurer.setSqlSessionFactoryBeanName("the_factory_1");
return configurer;
}
However, that leads me to this error:
No qualifying bean of type [com.myproject.package1.mapper.MyMapper] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
When I debug only one #Bean for the factory gets invoked.
UPDATE 2
If I move everything to a single file all is fine. However, that is not ideal as I want the DataSource definitions to be separated. That's my only hurdle right now.

You can use ace-mybatis, it simplifies configuration.
Add one bean.
#Bean
public static AceMapperScannerConfigurer mapperScannerConfigurer() {
return AceMapperScannerConfigurer.builder()
.basePackage("com.myproject.package1")
.build();
}
And then mark your mapper interfaces with #AceMapper and specify sqlSessionFactory
#AceMapper(sqlSessionFactoryBeanName = "firstSqlSessionFactory")
public interface UserMapper {
Stream<User> selectUsers();
}
#AceMapper(sqlSessionFactoryBeanName = "secondSqlSessionFactory")
public interface ClientMapper {
Stream<Client> selectClients();
}

Please use DAOFactory pattern to get connections for multiple datasources like DS1 and DS2 and use DAOUtil class to provide required configuration using annotation

Related

Error creating bean with name 'batchDataSource': Requested bean is currently in creation: Is there an unresolvable circular reference?

I have a batch configuration. I saw the batch process is default using InMemoryMap. Instead I need to use the MySQL to send all the execution details by Batch. But when I use the following code I am getting the following error,
Error creating bean with name 'batchDataSource': Requested bean is
currently in creation: Is there an unresolvable circular reference?
#Configuration
#EnableBatchProcessing
public class BatchProcess extends DefaultBatchConfigurer {
private #Autowired Environment env;
#Bean
#StepScope
public ItemReader reader() {
...
}
#Bean
#StepScope
public ItemProcessor processor() {
...
}
#Bean
#StepScope
public ItemWriter writer() {
...
}
#Bean
#Primary
public DataSource batchDataSource() {
HikariDataSource hikari = new HikariDataSource();
hikari.setDriverClassName(env.getProperty("spring.datasource.driver-class-name"));
hikari.setJdbcUrl(env.getProperty("spring.datasource.url"));
hikari.setUsername(env.getProperty("spring.datasource.username"));
hikari.setPassword(env.getProperty("spring.datasource.password"));
return hikari;
}
public JobRepository getJobRepository() {
JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
factory.setDataSource(batchDataSource());
factory.setTransactionManager(manager());
factory.afterPropertiesSet();
return factory.getObject();
}
public PlatformTransactionManager manager() {
return new ResourcelessTransactionManager();
}
#Bean
public Step step() {
return stepBuilderFactory.get("step")
.chunk(1000)
.reader(reader())
.processor(processor())
.writer(writer())
.build();
}
#Bean
public Job job() {
return jobBuilderFactory.get("job")
.flow(step())
.end()
.build();
}
#Bean
public JobLauncher getJobLauncher() {
SimpleJobLauncher launcher = new SimpleJobLauncher();
launcher.setJobRepository(createJobRepository());
return launcher;
}
}
In property file I am using,
spring.batch.job.enabled=false
spring.batch.initialize-schema=always
So what I missed? I am using JPA. And even why it is not using available JPA datasource? How can I force the Spring batch to use default MySQL instead InMemoryMap?
The error message you are receiving may not be the clearest, but it should point you in the right direction. You appear to have a circular dependency within your code.
This happens when you have two (or more) beans that mutually depend upon one another, preventing the creation of one without the existence of the other (and vice versa) - the proverbial chicken and egg problem. You can generally avoid this with setter injection and some kind of post-construction initialization.
I think you have created this situation by extending DefaultBatchConfigurer and then defining the #Bean annotated method getJobLauncher() which directly calls DefaultBatchConfigurer's createJobRepository() method without ensuring that the DataSource is first set within DefaultBatchConfigurer.
This is entirely unnecessary, because DefaultBatchConfigurer already creates JobRepository, JobExplorer, and JobLauncher for you in the proper order.
From DefaultBatchConfigurer:
#PostConstruct
public void initialize() {
try {
this.jobRepository = createJobRepository();
this.jobExplorer = createJobExplorer();
this.jobLauncher = createJobLauncher();
} catch (Exception e) {
throw new BatchConfigurationException(e);
}
}
If you are going to extend DefaultBatchConfigurer, then I suggest you eliminate the following methods from your code:
getJobRepository()
manager()
getJobLauncher()
From your code sample, it appears that you are already setting the following properties (in your application.properties file?):
spring.datasource.jdbcUrl=...
spring.datasource.username=...
spring.datasource.password=...
spring.datasource.driverClassName=...
That should be sufficient to allow Spring's AutoConfiguration to create an Hikari DataSource for you automatically, and this is the approach I usually take. The Spring Bean name will be dataSource, and this will be autowired into DefaultBatchConfigurer via setDataSource().
However, in your code sample, you have also defined a #Bean annotated method named batchDataSource(), which looks no different to what you should receive from Spring AutoConfiguration. As long as you have the spring.datasource properties mentioned earlier configured, you should be able to eliminate batchDataSource() as well, but I don't think that's necessary, so your choice.
If you still want to manually configure your DataSource, then I suggest that you not extend DefaultBatchConfigurer, but instead define a custom bean for it in a configuration class where you can directly pass in your custom DataSource (based on what I currently know of your use case).
#Bean
public BatchConfigurer batchConfigurer(){
return new DefaultBatchConfigurer( batchDataSource() );
}
First of all, explicitly define the mysql-connector dependency in the pom.xml and remove anything related to in-memory map from the project.
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
If you want to define your own configuration with beans manually, then you can't use the AutoConfiguration classes because they create the required beans for you on startup automatically and that might cause an issue if you are defining own custom DB configuration classes. Therefore, you have to exclude DataSourceAutoConfiguration, HibernateJpaAutoConfiguration and DataSourceTransactionManagerAutoConfiguration to resolve the issue.
Just update the #SpringBootApplication class :
#SpringBootApplication(
exclude = {DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class
}
)
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}

Spring boot Mybatis multiple datasource

I am using Spring boot 2.0.3 and mybatis with PostgreSql.
I am trying to set up multiple data source connection as follows by following https://programmer.help/blogs/spring-boot-integrates-mybatis-multiple-data-sources.html.
Datasource1
#Configuration
#MapperScan(basePackages = "com.repositories.StaRepository", sqlSessionFactoryRef = "sqlPromptSessionFactory", annotationClass = Mapper.class)
//SqlSessionFactory is created from DB1 and then a SqlSessionTemplate is created from the created SqlSessionFactory.
public class MyBatisConfigPrompt {
#Bean(name = "DB1")
#ConfigurationProperties(prefix = "spring.datasource.pro")
public DruidDataSource DB1() {
return DruidDataSourceBuilder.create().build();
}
#Bean(name = "sqlProSessionFactory")
SqlSessionFactory sqlProSessionFactory() {
SqlSessionFactory sessionFactory = null;
try {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(DB1());
sessionFactory = bean.getObject();
} catch (Exception e) {
e.printStackTrace();
}
return sessionFactory;
}
#Bean
public MapperScannerConfigurer proMapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setBasePackage("com.repositories.StaRepository");
configurer.setSqlSessionFactoryBeanName("sqlProSessionFactory");
return configurer;
}
}
Datasource2
#Configuration
#MapperScan(basePackages = "com.repositories.ContDBRepository", sqlSessionFactoryRef = "sqlContSessionFactory", annotationClass = Mapper.class)
//SqlSessionFactory is created from contDB and then a SqlSessionTemplate is created from the created SqlSessionFactory.
public class MyBatisConfigCont {
#Bean(name = "contDB")
#ConfigurationProperties(prefix = "spring.datasource.cont")
public DruidDataSource contDB() {
return DruidDataSourceBuilder.create().build();
}
#Bean(name = "sqlContSessionFactory")
SqlSessionFactory sqlContSessionFactory() {
SqlSessionFactory sessionFactory = null;
try {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(contDB());
sessionFactory = bean.getObject();
} catch (Exception e) {
e.printStackTrace();
}
return sessionFactory;
}
#Bean
public MapperScannerConfigurer contMapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setBasePackage("com.repositories.ContDBRepository");
configurer.setSqlSessionFactoryBeanName("sqlContSessionFactory");
return configurer;
}
}
I have also a ContDBRepository.class with #Mapper Annotation and ContDBRepository.xml and same as StaRepository.class with #Mapper Annotation and StaRepository.xml in same package.
With the above configuration i am getting ERROR
No qualifying bean of type 'org.apache.ibatis.session.SqlSessionFactory' available: expected single matching bean but found 2: sqlContSessionFactory,sqlProSessionFactory
As a fix to the above error i set #Primary to one of the SqlSessionFactory but other SqlSessionFactory is never called when i want to use second datasource.
Can anyone help what i am missing.
UPDATE 20210301
This example help me to find a solution for makeing sure I could use the specific datasource.
The basic idea is to create a abstract data source as the router giving to the mybatis config. Then use a enum and #interface as the selector and adding them before any interface you want a specific data source. Finally AOP is the program paradigm to define how to change the data source.
Some key points:
AbstractRoutingDataSource will be the key to store our whole datasources.
#interface will be the key to create our router for our different ServiceImpl with specific interface, which will not need #Repository anymore, by adding that to any interface you override with the specific data source type.
#Aspect and #Pointcut will be the key to guarantee our router will work properly.
ORIGINAL
I found the same question for most example online about multiple data source.
The example you saw is not checked very much because he only use two localhost with same database info and table info.
The config example used:
spring.datasource.one.url=jdbc:mysql://localhost:3306/test01?useUnicode=true&characterEncoding=utf-8
spring.datasource.one.username=root
spring.datasource.one.password=123456
spring.datasource.one.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.two.url=jdbc:mysql://localhost:3306/test02?useUnicode=true&characterEncoding=utf-8
spring.datasource.two.username=root
spring.datasource.two.password=123456
spring.datasource.two.type=com.alibaba.druid.pool.DruidDataSource
All the example I tried, all failed at get table from wrong database, which actually means the #Repository only can get one DataSource or config.

mybatis #MapperScan not working

I'm trying to set up the Java config for MyBatis & the #MapperScan does not appear to be accomplishing anything. Note, I can get the application to work with XML config.
What am I missing? The com.test.mapper package definitely exists & has a file/iterface called TestMapper. The corresponding xml is in the correct location in the resources folder.
*************************** APPLICATION FAILED TO START
Description:
Field templateMapper in
com.test.TestController required a
bean of type 'com.test.mapper.TestMapper' that
could not be found.
Action:
Consider defining a bean of type
'com.test.mapper.TestMapper' in your
configuration.
Autowired that is failing
#Autowired
TestMapper _testMapper;
config
#Configuration
#MapperScan("com.test.mapper")
public class AppConfig {
#Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
try {
dataSource.setDriverClass(com.microsoft.sqlserver.jdbc.SQLServerDriver.class);
//dataSource.setDriverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
dataSource.setUrl("jdbc:sqlserver://server;databaseName=db1;integratedSecurity=true;");
} catch (Exception e) {
}
return dataSource;
}
#Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
#Bean
public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setTypeAliasesPackage("com.test.domain");
sqlSessionFactoryBean.setDataSource(dataSource());
return sqlSessionFactoryBean;
}
}
I solved this. My issue wasn't with Mybatis. It was with Spring. This link to the Spring docs says to "...locate your main application class in a root package above other classes".
I had not done that. Once I moved application class ( annotated with SpringBootApplication) then the #MapperScan annotation worked.

#Primary on Factory Beans

The simplified version I have looks like this:
#Configuration
#EnableTransactionManagement
public class DatabaseDefaultConfig {
#Bean
#Primary
public DataSource dataSourceDefault(DatabaseConfigurationHelper databaseConfigurationHelper) {
return ...;
}
#Bean
#Primary
public SqlSessionFactoryBean sqlSessionFactoryBeanDefault(DatabaseConfigurationHelper databaseConfigurationHelper, #Value("${datasource.default.cacheEnabled}") boolean cacheEnabled) throws Exception {
return ...;
}
}
#Configuration
#EnableTransactionManagement
public class DatabaseMaintenanceConfig {
#Bean
public DataSource dataSourceMaintenance(DatabaseConfigurationHelper databaseConfigurationHelper) {
return ...;
}
#Bean
public SqlSessionFactoryBean sqlSessionFactoryBeanMaintenance(DatabaseConfigurationHelper databaseConfigurationHelper, #Value("${datasource.maintenance.cacheEnabled}") boolean cacheEnabled) throws Exception {
return ...;
}
}
The classes are very much the same, one uses #Primary. Now let's create two dummy beans:
#Configuration
public class CommonDatabaseConfig {
#Bean
public AtomicInteger a(SqlSessionFactoryBean sqlSessionFactoryBean) {
return new AtomicInteger();
}
#Bean
public AtomicLong b(DataSource dataSource) {
return new AtomicLong();
}
}
While b works fine, a fails and claims that two beans were found:
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of method a in sjngm.CommonDatabaseConfig required a single bean, but 2 were found:
- &sqlSessionFactoryBeanDefault: defined by method 'sqlSessionFactoryBeanDefault' in class path resource [sjngm/DatabaseDefaultConfig.class]
- &sqlSessionFactoryBeanMaintenance: defined by method 'sqlSessionFactoryBeanMaintenance' in class path resource [sjngm/DatabaseMaintenanceConfig.class]
Action:
Consider marking one of the beans as #Primary, updating the consumer to accept multiple beans, or using #Qualifier to identify the bean that should be consumed
Note that both beans start with a &. Reading this question and its answer it becomes clear that this is intended. However, that seems to break applying the #Primary as it fails in this area of Spring's DefaultListableBeanFactory:
protected boolean isPrimary(String beanName, Object beanInstance) {
if (containsBeanDefinition(beanName)) {
return getMergedLocalBeanDefinition(beanName).isPrimary();
}
BeanFactory parent = getParentBeanFactory();
return (parent instanceof DefaultListableBeanFactory &&
((DefaultListableBeanFactory) parent).isPrimary(beanName, beanInstance));
}
containsBeanDefinition() in line 2 returns false because of the ampersand.
Now: Am I doing something wrong here? How can I fix this?
This is Spring 4.3.9 (as part of Spring-Boot 1.5.4)
It's fixed within spring-framework PR 22711.

Spring with MyBatis: expected single matching bean but found 2

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

Categories

Resources