Spring Cloud Task - specify database config - spring

I have Spring Cloud Task that loads data from SQL Server to Cassandra DB which will be run on Spring Cloud Data Flow.
One of the requirement of Spring Task is to provide relational database to persist metadata like task execution state. But I don't want use either of the above databases for that. Instead, I have to specify third database for persistence. But it seems like Spring Cloud Task flow automatically picks up data source properties of SQL Server from application.properties. How can I specify another db for task state persistence?
My Current properties:
spring.datasource.url=jdbc:sqlserver://iphost;databaseName=dbname
spring.datasource.username=user
spring.datasource.password=password
spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.jpa.show-sql=false
#spring.jpa.hibernate.dialect=org.hibernate.dialect.SQLServer2012Dialect
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.hibernate.ddl-auto=none
spring.data.cassandra.contact-points=ip
spring.data.cassandra.port=9042
spring.data.cassandra.username=username
spring.data.cassandra.password=password
spring.data.cassandra.keyspace-name=mykeyspace
spring.data.cassandra.schema-action=CREATE_IF_NOT_EXISTS
Update: 1
I added below code to point to 3rd database as suggested by Michael Minella. Now Spring Task is able to connect to this DB and persist state. But now my batch job source queries are also connecting to this database. Only thing I changed was to add datasource for task.
spring.task.datasource.url=jdbc:postgresql://host:5432/testdb?stringtype=unspecified
spring.task.datasource.username=user
spring.task.datasource.password=passwrod
spring.task.datasource.driverClassName=org.postgresql.Driver
#Configuration
public class DataSourceConfigs {
#Bean(name = "taskDataSource")
#ConfigurationProperties(prefix="spring.task.datasource")
public DataSource getDataSource() {
return DataSourceBuilder.create().build();
}
}
#Configuration
public class DDTaskConfigurer extends DefaultTaskConfigurer{
#Autowired
public DDTaskConfigurer(#Qualifier("taskDataSource") DataSource dataSource) {
super(dataSource);
}
}
Update #2:
#Component
#StepScope
public class MyItemReader extends RepositoryItemReader<Scan> implements InitializingBean{
#Autowired
private ScanRepository repository;
private Integer lastScanIdPulled = null;
public MyItemReader(Integer _lastIdPulled) {
super();
if(_lastIdPulled == null || _lastIdPulled <=0 ){
lastScanIdPulled = 0;
} else {
lastScanIdPulled = _lastIdPulled;
}
}
#PostConstruct
protected void setUpRepo() {
final Map<String, Sort.Direction> sorts = new HashMap<>();
sorts.put("id", Direction.ASC);
this.setRepository(this.repository);
this.setSort(sorts);
this.setMethodName("findByScanGreaterThanId");
List<Object> methodArgs = new ArrayList<Object>();
System.out.println("lastScanIdpulled >>> " + lastScanIdPulled);
if(lastScanIdPulled == null || lastScanIdPulled <=0 ){
lastScanIdPulled = 0;
}
methodArgs.add(lastScanIdPulled);
this.setArguments(methodArgs);
}
}
#Repository
public interface ScanRepository extends JpaRepository<Scan, Integer> {
#Query("...")
Page<Scan> findAllScan(final Pageable pageable);
#Query("...")
Page<Scan> findByScanGreaterThanId(int id, final Pageable pageable);
}
Update #3:
If I add config datasource for Repository, I now get below exception. Before you mention that one of the datasource needs to be declared Primary. I already tried that.
Caused by: java.lang.IllegalStateException: Expected one datasource and found 2
at org.springframework.cloud.task.batch.configuration.TaskBatchAutoConfiguration$TaskBatchExecutionListenerAutoconfiguration.taskBatchExecutionListener(TaskBatchAutoConfiguration.java:65) ~[spring-cloud-task-batch-1.0.3.RELEASE.jar:1.0.3.RELEASE]
at org.springframework.cloud.task.batch.configuration.TaskBatchAutoConfiguration$TaskBatchExecutionListenerAutoconfiguration$$EnhancerBySpringCGLIB$$baeae6b9.CGLIB$taskBatchExecutionListener$0(<generated>) ~[spring-cloud-task-batch-1.0.3.RELEASE.jar:1.0.3.RELEASE]
at org.springframework.cloud.task.batch.configuration.TaskBatchAutoConfiguration$TaskBatchExecutionListenerAutoconfiguration$$EnhancerBySpringCGLIB$$baeae6b9$$FastClassBySpringCGLIB$$5a898c9.invoke(<generated>) ~[spring-cloud-task-batch-1.0.3.RELEASE.jar:1.0.3.RELEASE]
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228) ~[spring-core-4.3.14.RELEASE.jar:4.3.14.RELEASE]
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:358) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
at org.springframework.cloud.task.batch.configuration.TaskBatchAutoConfigu
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = "myEntityManagerFactory",
basePackages = { "com.company.dd.collector.tool" },
transactionManagerRef = "TransactionManager"
)
public class ToolDbConfig {
#Bean(name = "myEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean
myEntityManagerFactory(
EntityManagerFactoryBuilder builder,
#Qualifier("ToolDataSource") DataSource dataSource
) {
return builder
.dataSource(dataSource)
.packages("com.company.dd.collector.tool")
.persistenceUnit("tooldatasource")
.build();
}
#Bean(name = "myTransactionManager")
public PlatformTransactionManager transactionManager(
#Qualifier("myEntityManagerFactory") EntityManagerFactory
entityManagerFactory
) {
return new JpaTransactionManager(entityManagerFactory);
}
}
#Configuration
public class DataSourceConfigs {
#Bean(name = "taskDataSource")
#ConfigurationProperties(prefix="spring.task.datasource")
public DataSource getDataSource() {
return DataSourceBuilder.create().build();
}
#Primary
#Bean(name = "ToolDataSource")
#ConfigurationProperties(prefix = "tool.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
}

You need to create a TaskConfigurer to specify the DataSource to be used. You can read about this interface in the documentation here: https://docs.spring.io/spring-cloud-task/1.1.1.RELEASE/reference/htmlsingle/#features-task-configurer
The javadoc can be found here: https://docs.spring.io/spring-cloud-task/docs/current/apidocs/org/springframework/cloud/task/configuration/TaskConfigurer.html
UPDATE 1:
When using more than one DataSource, both Spring Batch and Spring Cloud Task follow the same paradigm in that they both have *Configurer interfaces that need to be used to specify what DataSource to use. For Spring Batch, you use the BatchConfigurer (typically by just extending the DefaultBatchConfigurer) and as noted above, the TaskConfigurer is used in Spring Cloud Task. This is because when there is more than one DataSource, the framework has no way of knowing which one to use.

Related

How do I configure two databases in Spring Boot?

I am all new to Spring Boot and I have read som documentation about how you create a Spring boot application.
I have created an application in Spring Boot, this application should run a Spring Batch job (I am rewriting an old job in Spring Batch to a stand alone Spring Boot applikation). I have created the structure of the job with steps and so on. All the job does right now is moving files and that works. I work with embedded databases during development, h2, and Spring Boot has generated the whole database for Spring Batch. Really nice :)
So now my problem, in one of the steps I have to fetch and store data in another database. And I don't know (understand) how I should create this database and access the database in the job.
So in my application.properties
spring.h2.console.enabled=true
spring.h2.console.path=/h2
spring.datasource.url=jdbc:h2:mem:springdb
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
persons.h2.console.enabled=true
persons.h2.console.path=/h2
persons.datasource.url=jdbc:h2:mem:persons
persons.datasource.username=sa
persons.datasource.password=
persons.datasource.driverClassName=org.h2.Driver
In test I will change database to a SQL-database on another server.
I have some entitys (example)
public class Person {
private Long id;
private String name;
private String familyname;
private Long birthDate;
public Person () {
}
...with getters and setters
In the configuration I have
#Primary
#Bean
#ConfigurationProperties(prefix = "persons.datasource")
public DataSource personsDataSource() {
return DataSourceBuilder.create().build();
}
#Bean
#ConfigurationProperties(prefix = "spring.datasource")
public DataSource batchDataSource() {
return DataSourceBuilder.create().build();
}
And the job
#Autowired
PersonItemWriter itemWriter;
#Autowired
PersonItemProcessor itemProcessor;
#Autowired
PersonItemReader workReader;
#Bean(name = "personsImportJob")
public Job personImportJob() {
Step downloadFiles = stepBuilderFactory.get("download-files")
.tasklet(downloadTasklet())
.build();
Step syncDbAndSavePersons = stepBuilderFactory.get("syncandsave-persons")
.<File, Person>chunk(50)
.reader(workReader)
.processor(itemProcessor)
.writer(itemWriter)
.build();
Step deleteFiles = stepBuilderFactory.get("delete-files")
.tasklet(deleteTasklet())
.build();
Job job = jobBuilderFactory.get("personsimport-job")
.incrementer(new RunIdIncrementer())
.flow(downloadFiles)
.next(syncDbAndSavePersons)
.next(deleteFiles)
.end()
.build();
return job;
}
My writer
#Component
public class PersonItemWriter implements ItemWriter<Person> {
private static final Logger LOGGER = LoggerFactory.getLogger(PersonItemWriter.class);
#Override
public void write(List<? extends Person> list) throws Exception {
LOGGER.info("Write Person to db");
}
}
Now this works, the step syncAndSavePersons does not do anything right now, but I want this step to access another database and update posts to the persons-database.
Can I do this without JPA? Because the existing job doesn't use JPA and if I have to use JPA there will be a lot of code changes and I want to avoid this. I just want to move the job with minimum of changes,
If I run my application with this the only database that is created is the database for spring batch. How can I make sure that the other database is also created? Or is that impossible when I am working with h2 embedded databases?
Before I added the second database I didn't have the datasource configuration at all in my code, but the database was created anyway. I think Spring Boot just created the batch datasource. So maybe I won't need that configuration?
UPDATE:
I solved it by removing the properties for the database in te properties file. The only thing I left is:
spring:
datasource:
initialization-mode: never
h2:
console:
enabled: true
I then created a class called EmbeddedDataSourceConfig:
#Profile("default")
#Configuration
public class EmbeddedDataSourceConfig {
#Bean
#Primary
public DataSource dataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:/org/springframework/batch/core/schema-h2.sql")
.build();
}
#Bean(name = "personsDataSource")
public DataSource personsDataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder
.setName("persons")
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:persons_schema.sql")
.build();
}
#Bean(name = "personsTransactionManager")
public PlatformTransactionManager personsTransactionManager(
#Qualifier("personsDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
#Bean(name = "personsJdbcTemplate")
public JdbcTemplate personsJdbcTemplate(#Qualifier("personsDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
and in the job I changed to following
#Bean(name = "personsImportJob")
public Job personImportJob(#Qualifier("personsTransactionManager") PlatformTransactionManager personsTransactionManager) {
Step downloadFiles = stepBuilderFactory.get("download-files")
.tasklet(downloadTasklet())
.build();
Step syncDbAndSavePersons = stepBuilderFactory.get("syncandsave-persons")
.<File, Person>chunk(50)
.reader(workReader)
.processor(itemProcessor)
.writer(itemWriter)
.transactionManager(personsTransactionManager)
.build();
...
}
That's it. Now it generates two h2 in-memory databases.

Non-primary embedded database schema not being initiliazed

I'm currently working on a Spring Boot (1.5.10.RELEASE) application that uses 2 embedded databases. This two databases are defined equally but one's defined on the application itself and one's defined on a custom autoconfiguration class that's on an imported JAR; yet only the one flagged as #Primary (the one on the application) gets the schema initialized.
This is the current definition for both datasources:
Primary, on the application:
#Configuration
public class DataSourceConfiguration {
#Bean
#ConfigurationProperties(prefix = "first.datasource")
#Primary
public DataSourceProperties firstProperties() {
return new DataSourceProperties();
}
#Bean
#Primary
public DataSource firstDataSource() {
return firstProperties().initializeDataSourceBuilder().build();
}
#Bean
#Primary
public JdbcTemplate firstTemplate() {
return new JdbcTemplate(firstDataSource());
}
}
On the autoconfiguration:
#Configuration
#ConditionalOnProperty(name = "second.datasource.url")
public class SecondDataSourceAutoconfiguration {
#Bean
#ConfigurationProperties(prefix = "second.datasource")
public DataSourceProperties secondProperties() {
return new DataSourceProperties();
}
#Bean
public DataSource secondDataSource() {
return secondProperties().initializeDataSourceBuilder().build();
}
#Bean
public JdbcTemplate secondJdbcTemplate(#Qualifier("secondDataSource") DataSource datasource) {
return new JdbcTemplate(datasource);
}
}
And my application.yml fills the properties:
first:
datasource:
url: jdbc:h2:firstdb;DB_CLOSE_ON_EXIT=FALSE
second:
datasource:
url: jdbc:h2:seconddb
platform: h2
My resources folder contains both a schema.sql that's executed on firstdb and a schema-h2.sql which should get executed on seconddb but does not. I tried playing around with the datasource.schema and datasource.initialize properties, switching the script names and the platform property to the first datasource (in that case, the schema-h2.sql gets executed on firstdb, but nothing on the seconddb) and changing the embedded database provider to HSQLDB; but can't get the schema for the non-primary in-memory DB initialized anyhow.

Spring boot connection to more then 2 data source

I'm currently working on Spring Boot project where I need to connect to more than 2 data source (actually 4).
I found many examples how to connect to 2 DS and it works, but when I add next in the same way it's not working:
...Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Not a managed type: class...
Is there any restriction on data sources?
Or is it possible to connect to more than 2 DS?
Create as many datasource you want. No restrictions i am aware of. It is just another bean .Refer here
#first db
spring.datasource.url = [url]
spring.datasource.username = [username]
spring.datasource.password = [password]
spring.datasource.driverClassName = oracle.jdbc.OracleDriver
#second db ...
spring.secondDatasource.url = [url]
spring.secondDatasource.username = [username]
spring.secondDatasource.password = [password]
spring.secondDatasource.driverClassName = oracle.jdbc.OracleDriver
#Bean("firstds")
#ConfigurationProperties(prefix="spring.datasource")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
#Bean("secondds")
#ConfigurationProperties(prefix="spring.secondDatasource")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
Adding to what #Surya said
Step 1: Set up the data base configuration in application.properties file as mentioned above.
Step 2:You need to create a bean.In your case u need to create 2 separate bean pointing to 2 diff data source.
#Bean("firstds")
#ConfigurationProperties(prefix="spring.datasource")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
#Bean("secondds")
#ConfigurationProperties(prefix="spring.secondDatasource")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
U can create this 2 bean in a single class.
Now wondering how to use this bean!
#Autowired
#Qualifier("firstds")
NamedParameterJdbcTemplate firstConnection;
result = firstConnection.query(Your query goes here)
Above query will always hit first db via the bean you created.
Similarly you can use connection to hit db 2 via the second bean.
Thanks for your help.
Actually I did all configuration right but ... (one small misspell crash all :))
As I was starting including my code I found it, fixed and now all works fine.
But here is my code as example (maybe someone will use it :))
All three entities and repositories (Shadow, Main, Project) are configured exactly in the same way so I put it only once. (the only differences is that DS for Main is as #Primary.
Thanks.
BR
application.properties
#-------------------------- first
spring.datasource.url=jdbc:mysql://
spring.datasource.username=
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#-------------------------- second
second.datasource.url=jdbc:mysql://
second.datasource.username=
second.datasource.password=
second.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#-------------------------- third
third.datasource.url=jdbc:mysql://
third.datasource.password=
third.datasource.username=
third.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
Shadow.java
package com.server.shadow.domain;
import ...
#Entity
#Table(name = "shadow")
public class Shadow {
#Id
private String id;
private String shadow;
public Shadow(){}
//getters and setters
}
ShadowRepository.java
package com.server.shadow.repo;
import ...
#Repository
public interface ShadowRepository extends JpaRepository<Shadow,Long> {
}
ShadowDbConf.java
package com.server;
import ...
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(entityManagerFactoryRef = "shadowEntityManagerFactory",
transactionManagerRef = "shadowTransactionManager",
basePackages = {"com.server.shadow.repo"}
)
public class ShadowDbConf {
#Bean(name = "shadowDataSource")
#ConfigurationProperties(prefix = "second.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
#Bean(name = "shadowEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean
shadowEntityManagerFactory(
EntityManagerFactoryBuilder builder,
#Qualifier("shadowDataSource") DataSource dataSource
) {
return
builder
.dataSource(dataSource)
.packages("com.server.shadow.domain")
.persistenceUnit("shadow")
.build();
}
#Bean(name = "shadowTransactionManager")
public PlatformTransactionManager shadowTransactionManager(
#Qualifier("shadowEntityManagerFactory") EntityManagerFactory
shadowEntityManagerFactory
) {
return new JpaTransactionManager(shadowEntityManagerFactory);
}
}
MainController.java
package com.server;
import ...
#RestController
public class MainController {
private final ShadowRepository shadowRepository;
private final MainRepository mainRepository;
private final PojectRepository projectRepository;
#Autowired
MainController(ShadowRepository shadowRepository, MainRepository mainRepository,PojectRepository projectRepository) {
this.shadowRepository = shadowRepository;
this.mainRepository = mainRepository;
this.projectRepository = projectRepository;
}
#GetMapping(path = "/shadow")
public #ResponseBody Iterable<Shadow> getShadows(){
return shadowRepository.findAll();
}
}

Spring batch boot Multiple datasources Multiple schemas

I have a spring batch job using spring boot which has 2 datasources. Each datasource again has 2 schemas each. I need to specify default schema for both the datasources. I know of property spring.jpa.properties.hibernate.default_schema which i am using to specify default schema for one datasource. Is there a way to specify default schema for another schema?
Currently, to specify default schema for the other datasource , i am using alter session query to switch schema as required. I am trying to get rid of this alter session query from my java code. Any suggestions on it is greatly appreciated.
edit 1: Both are ORACLE databases
If you use multiple datasources, then you probably has a #Configuration class for each datasource. In this case you can set additional properties to the entityManager. This configuration is needed:
props.put("spring.datasource.schema", "test");
Full example
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(entityManagerFactoryRef = "testEntityManagerFactory", transactionManagerRef = "testTransactionManager",
basePackages = {"com.test.repository"})
public class TestDbConfig {
#Bean(name = "testDataSource")
#ConfigurationProperties(prefix = "test.datasource")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
#Bean(name = "testEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, #Qualifier("testDataSource") DataSource dataSource) {
return builder.dataSource(dataSource).packages("com.test.model").persistenceUnit("test").properties(jpaProperties()).build();
}
private Map<String, Object> jpaProperties() {
Map<String, Object> props = new HashMap<>();
props.put("hibernate.physical_naming_strategy", SpringPhysicalNamingStrategy.class.getName());
props.put("hibernate.implicit_naming_strategy", SpringImplicitNamingStrategy.class.getName());
props.put("spring.datasource.schema", "test");
return props;
}
#Bean(name = "testTransactionManager")
public PlatformTransactionManager transactionManager(#Qualifier("testEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}

SpringBoot web services multiple data sources only one working

I have developed two webservices using Spring Boot framework and I have them in the same project. Each webservice use a different DB, say ws1 uses Oracle1 and ws2 uses Oracle2. I have defined a DataBaseConfig with the beans definition but when I run the app, always works one webservice ( and it's always the same ).
DataBaseConfig
#Configuration
public class DataBaseConfig {
#Bean(name = "ora1")
#ConfigurationProperties(prefix="spring.datasource")
public DataSource mysqlDataSource() {
return DataSourceBuilder.create().build();}
#Bean(name = "ora2")
#ConfigurationProperties(prefix="spring.secondDatasource")
public DataSource sqliteDataSource() {
return DataSourceBuilder.create().build();}
#Bean(name = "clients")
#Autowired
#ConfigurationProperties(prefix = "spring.datasource")
#Qualifier("datasource")
public JdbcTemplate slaveJdbcTemplate(DataSource datasource) {
return new JdbcTemplate(datasource); }
#Bean(name = "places")
#Autowired
#Primary
#ConfigurationProperties(prefix = "spring.secondDatasource")
#Qualifier("secondDatasource")
public JdbcTemplate masterJdbcTemplate(DataSource secondDatasource) {
return new JdbcTemplate(secondDatasource);}
}
I have the services definition with the sql statements and the definition
#Service
public class ClientsService {
#Autowired
#Qualifier("clients")
private JdbcTemplate jdbcTemplate;
and the other service
#Service
public class PlacesService {
#Autowired
#Qualifier("places")
private JdbcTemplate jdbcTemplate;
Then in each controller I have de mapping #RequestMapping. When I run the app I have no connection-related errors and if I separate the webservices in 2 projects, each works fine.
You have a few things going wrong here, including some unnecessary annotations. See below, note the location of #Qualifier and the qualifier name:
#Bean(name = "clients")
public JdbcTemplate slaveJdbcTemplate(#Qualifier("ora1") DataSource datasource) {
return new JdbcTemplate(datasource);
}
#Bean(name = "places")
#Primary
public JdbcTemplate masterJdbcTemplate(#Qualifier("ora2") DataSource secondDatasource) {
return new JdbcTemplate(secondDatasource);
}
Instead of resolving by bean name, which is a bad idea IMO because it's not typesafe, why don't you use constructor injection and create the services in the configuration class (ditch #Service annotation). Create the DataSource and JdbcTemplate beans as usual, don't give them any names (the default is the method name), and also create new PlacesService(placesJdbcTemplate()). The result is a lot simpler code.
This is assuming you want both databases active at runtime. If not, use #Profile.

Resources