#Transaction not working on multi datasource Spring Boot + MyBatis application - spring

I'm trying to configure Spring Boot + MyBatis application which should work with several datasources. I tried to do it similar to sample here.
Querying and updating data is working, but when I wrote the unit test, I found that #Transactional is not working. Transactions must work between all databases. It means that, if one method with #Transactional make updates on both databases then everything should rollback in case of exception.
It is a sample application for testing purposes and co-workers. After successful configuring the new applications will be configured and developed in similar manner.
Maven:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.aze.mybatis</groupId>
<artifactId>sample-one</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Below are the configuration classes:
package com.aze.mybatis.sampleone;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
#SpringBootApplication
#EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
Config to database BSCS (Oracle)
package com.aze.mybatis.sampleone.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
#Configuration
#MapperScan(basePackages = "com.aze.mybatis.sampleone.dao", annotationClass = BscsDataSource.class, sqlSessionFactoryRef = BscsDatabaseConfig.SQL_SESSION_FACTORY_NAME)
#EnableTransactionManagement
public class BscsDatabaseConfig {
static final String SQL_SESSION_FACTORY_NAME = "sessionFactoryBscs";
private static final String TX_MANAGER = "txManagerBscs";
#Bean(name = "dataSourceBscs")
#ConfigurationProperties(prefix = "bscs.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
#Bean(name = TX_MANAGER)
public PlatformTransactionManager txManagerBscs() {
return new DataSourceTransactionManager(dataSource());
}
#Bean(name = BscsDatabaseConfig.SQL_SESSION_FACTORY_NAME)
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
return sqlSessionFactoryBean.getObject();
}
}
Config to database ONSUBS (Oracle)
package com.aze.mybatis.sampleone.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
#Configuration
#MapperScan(basePackages = "com.aze.mybatis.sampleone.dao", annotationClass = OnsubsDataSource.class, sqlSessionFactoryRef = OnsubsDatabaseConfig.SQL_SESSION_FACTORY_NAME)
#EnableTransactionManagement
public class OnsubsDatabaseConfig {
static final String SQL_SESSION_FACTORY_NAME = "sessionFactoryOnsubs";
private static final String TX_MANAGER = "txManagerOnsubs";
#Bean(name = "dataSourceOnsubs")
#Primary
#ConfigurationProperties(prefix = "onsubs.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
#Bean(name = TX_MANAGER)
#Primary
public PlatformTransactionManager txManagerOnsubs() {
return new DataSourceTransactionManager(dataSource());
}
#Bean(name = OnsubsDatabaseConfig.SQL_SESSION_FACTORY_NAME)
#Primary
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
return sqlSessionFactoryBean.getObject();
}
}
Annotation for BSCS:
package com.aze.mybatis.sampleone.config;
public #interface BscsDataSource {
}
and ONSUBS:
package com.aze.mybatis.sampleone.config;
public #interface OnsubsDataSource {
}
Mapper interface that should work with ONSUBS:
package com.aze.mybatis.sampleone.dao;
import com.aze.mybatis.sampleone.config.OnsubsDataSource;
import com.aze.mybatis.sampleone.domain.Payment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
#Mapper
#OnsubsDataSource
public interface PaymentDao {
Payment getPaymentById(#Param("paymentId") Integer paymentId);
}
and BSCS:
package com.aze.mybatis.sampleone.dao;
import com.aze.mybatis.sampleone.config.BscsDataSource;
import com.aze.mybatis.sampleone.domain.PostpaidBalance;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
#Mapper
#BscsDataSource
public interface PostpaidCustomerDao {
PostpaidBalance getPostpaidBalance(#Param("customerId") Integer customerId);
// BigDecimal amount may be used as second parameter, but I want to show, how to work with two parameters where second is object
void updateDepositAmount(#Param("customerId") Integer customerId, #Param("balance") PostpaidBalance postpaidBalance);
void updateAzFdlLastModUser(#Param("customerId") Integer customerId, #Param("username") String username);
}
Below is a code with #Transactional
package com.aze.mybatis.sampleone.service;
import com.aze.mybatis.sampleone.dao.PaymentDao;
import com.aze.mybatis.sampleone.dao.PostpaidCustomerDao;
import com.aze.mybatis.sampleone.domain.Payment;
import com.aze.mybatis.sampleone.domain.PostpaidBalance;
import com.aze.mybatis.sampleone.exception.DataNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
#Service
public class PaymentServiceImpl implements PaymentService {
private static final String MIN_DEPOSIT_AMOUNT = "150";
#Autowired
private PaymentDao paymentDao;
#Autowired
private PostpaidCustomerDao postpaidCustomerDao;
#Override
public PostpaidBalance getPostpaidBalance(Integer customerId) {
PostpaidBalance balance = postpaidCustomerDao.getPostpaidBalance(customerId);
if (balance == null) {
throw new DataNotFoundException(String.format("Can't find any balance information for customer with customer_id = %d", customerId));
}
return balance;
}
// Note. By default rolling back on RuntimeException and Error but not on checked exceptions
// If you want to rollback on check exception too then add "rollbackFor = Exception.class"
#Transactional(rollbackFor = Exception.class)
#Override
public void updateDepositAmount(Integer customerId, PostpaidBalance postpaidBalance, String username) {
postpaidCustomerDao.updateDepositAmount(customerId, postpaidBalance);
// In case of #Transactional annotation, you can use method from the same class if it doesn't change data on database
PostpaidBalance balance = getPostpaidBalance(customerId);
// This logic is for showing that how the #Transactional annotation works.
// Because of the exception, the previous transaction will rollback
if (balance.getDeposit().compareTo(new BigDecimal(MIN_DEPOSIT_AMOUNT)) == -1) {
throw new IllegalArgumentException("The customer can not have deposit less than " + MIN_DEPOSIT_AMOUNT);
}
// In case of #Transactional annotation, you must not (!!!) use method from the same (!) class if it changes data on database
// That is why, postpaidCustomerDao.updateAzFdlLastModUser() used here instead of this.updateAzFdlLastModUser()
postpaidCustomerDao.updateAzFdlLastModUser(customerId, username);
// If there is no exception, the transaction will commit
}
}
Below is the unit test code:
package com.aze.mybatis.sampleone.service;
import com.aze.mybatis.sampleone.Application;
import com.aze.mybatis.sampleone.config.BscsDatabaseConfig;
import com.aze.mybatis.sampleone.config.OnsubsDatabaseConfig;
import com.aze.mybatis.sampleone.domain.PostpaidBalance;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.math.BigDecimal;
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = {Application.class, OnsubsDatabaseConfig.class, BscsDatabaseConfig.class})
#TestPropertySource(locations= "classpath:application.properties")
public class PaymentServiceImplTest extends Assert {
// My goal is not to write a full and right unit tests, but just show you examples of working with MyBatis
#Autowired
private PaymentService paymentService;
#Before
public void setUp() throws Exception {
assert paymentService != null;
}
#Test
public void updateDepositAmount() throws Exception {
final int customerId = 4301887; // not recommended way. Just for sample
final String username = "ITCSC";
boolean exceptionRaised = false;
PostpaidBalance balance = paymentService.getPostpaidBalance(customerId);
assertTrue("Find customer with deposit = 0", balance.getDeposit().compareTo(BigDecimal.ZERO) == 0);
balance.setDeposit(BigDecimal.TEN);
try {
paymentService.updateDepositAmount(customerId, balance, username);
} catch (Exception e) {
exceptionRaised = true;
}
assertTrue(exceptionRaised);
balance = paymentService.getPostpaidBalance(customerId);
// We check that transaction was rollback and amount was not changed
assertTrue(balance.getDeposit().compareTo(BigDecimal.ZERO) == 0);
final BigDecimal minDepositAmount = new BigDecimal("150");
balance.setDeposit(minDepositAmount);
paymentService.updateDepositAmount(customerId, balance, username);
balance = paymentService.getPostpaidBalance(customerId);
assertTrue(balance.getDeposit().compareTo(minDepositAmount) != -1);
}
}
Unit test fails on assertTrue(balance.getDeposit().compareTo(BigDecimal.ZERO) == 0);. The I check the database and see that first update postpaidCustomerDao.updateDepositAmount(customerId, postpaidBalance); was not rollback despite the #Transactional annotation.
Please help to solve problem.

If you have multiple TransactionManagers, you'll need to reference the one you want to use for #Transactional using Bean names or Qualifiers.
In your Java Config:
#Bean("myTM")
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(myDatasource());
}
In Services:
#Transactional("myTM")
public void insertWithException(JdbcTemplate jdbcTemplate) {
}
I debug'd my app to see how Spring chooses the TransactionManager. This is done in org.springframework.transaction.interceptor.TransactionAspectSupport#determineTransactionManager
/**
* Determine the specific transaction manager to use for the given transaction.
*/
protected PlatformTransactionManager determineTransactionManager(TransactionAttribute txAttr) {
// Do not attempt to lookup tx manager if no tx attributes are set
if (txAttr == null || this.beanFactory == null) {
return getTransactionManager();
}
String qualifier = txAttr.getQualifier();
if (StringUtils.hasText(qualifier)) {
return determineQualifiedTransactionManager(qualifier);
}
else if (StringUtils.hasText(this.transactionManagerBeanName)) {
return determineQualifiedTransactionManager(this.transactionManagerBeanName);
}
else {
PlatformTransactionManager defaultTransactionManager = getTransactionManager();
if (defaultTransactionManager == null) {
defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
if (defaultTransactionManager == null) {
defaultTransactionManager = this.beanFactory.getBean(PlatformTransactionManager.class);
this.transactionManagerCache.putIfAbsent(
DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
}
}
return defaultTransactionManager;
}
}
So it's just getting the default primary PlatformTransactionManager bean.

Related

Issue in Kafka Spring boot test cases for AdminClient

I am writing unit test cases for below class . I'm trying to mock admin client , so that i can call the below method create topic. But getting null pointer exception.
#Service
public class TopicService {
private static final Logger LOG = LoggerFactory.getLogger(TopicService.class);
#Autowired
private AdminClient adminClient;
public void createTopic(Topic topic) throws ExecutionException, InterruptedException {
adminClient
.createTopics(Collections.singletonList(ServiceHelper.fromTopic(topic)))
.values()
.get(topic.getName())
.get();
}
}
The unit test case is as follows
package org.kafka.service;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.CreateTopicsResult;
import org.apache.kafka.clients.admin.ListTopicsResult;
import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.common.KafkaFuture;
import org.apache.kafka.common.internals.KafkaFutureImpl;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.kafka.model.Topic;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.*;
import java.util.concurrent.ExecutionException;
import static org.apache.kafka.clients.admin.AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG;
import static org.apache.kafka.clients.admin.AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG;
import static org.apache.kafka.common.internals.Topic.GROUP_METADATA_TOPIC_NAME;
#Slf4j
#RunWith(SpringRunner.class)
#SpringBootTest(classes = {TopicService.class})
public class TopicServiceTest {
#Autowired
TopicService topicService;
#MockBean
AdminClient adminClient;
ListTopicsResult listTopicsResult;
KafkaFuture<Set<String>> future;
NewTopic newTopic;
Topic topic;
Collection<NewTopic> topicList;
CreateTopicsResult createTopicsResult;
Void t;
Map<String,KafkaFuture<Void>> futureMap;
private static final String TARGET_CONSUMER_GROUP_ID = "target-group-id";
private static final Map<String, Object> CONF = new HashMap<>();
#BeforeClass
public static void createAdminClient() {
try {
CONF.put(BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
CONF.put(REQUEST_TIMEOUT_MS_CONFIG, 120000);
CONF.put("zookeeper.connect", "localhost:21891");
AdminClient adminClient = AdminClient.create(CONF);
} catch (Exception e) {
throw new RuntimeException("create kafka admin client error", e);
}
}
#Before
public void setUp(){
topicList = new ArrayList<>();
newTopic = new NewTopic("topic-7",1, (short) 1);
topicList.add(newTopic);
futureMap = new HashMap<>();
topic = new Topic();
topic.setName("topic-1");
}
#Test
public void createTopic() throws ExecutionException, InterruptedException {
Properties consumerProperties = new Properties();
Mockito.when(adminClient.createTopics(topicList))
.thenReturn(Mockito.mock(CreateTopicsResult.class));
Mockito.when(adminClient.createTopics(topicList).values())
.thenReturn(Mockito.mock(Map.class));
Mockito.when(adminClient.createTopics(topicList)
.values()
.get(GROUP_METADATA_TOPIC_NAME)).thenReturn(Mockito.mock(KafkaFutureImpl.class));
Mockito.when(adminClient.createTopics(topicList)
.values()
.get(GROUP_METADATA_TOPIC_NAME)
.get()).thenReturn(t);
topicService.createTopic(topic);
}
}
package org.kafka.config;
import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.kafka.reader.Kafka;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.core.KafkaAdmin;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
#Component
public class AdminConfigurer {
#Autowired
private Kafka kafkaConfig;
#Bean
public Map<String, Object> kafkaAdminProperties() {
final Map<String, Object> configs = new HashMap<>();
configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaConfig.getBootstrapServers());
if(kafkaConfig.getProperties().getSasl().getEnabled() && kafkaConfig.getSsl().getEnabled()) {
configs.put("sasl.mechanism", kafkaConfig.getProperties().getSasl().getMechanism());
configs.put("security.protocol", kafkaConfig.getProperties().getSasl().getSecurity().getProtocol());
configs.put("ssl.keystore.location", kafkaConfig.getSsl().getKeystoreLocation());
configs.put("ssl.keystore.password", kafkaConfig.getSsl().getKeystorePassword());
configs.put("ssl.truststore.location", kafkaConfig.getSsl().getTruststoreLocation());
configs.put("ssl.truststore.password", kafkaConfig.getSsl().getTruststorePassword());
configs.put("sasl.jaas.config", String.format(kafkaConfig.getJaasTemplate(),
kafkaConfig.getProperties().getSasl().getJaas().getConfig().getUsername(),
kafkaConfig.getProperties().getSasl().getJaas().getConfig().getPassword()));
configs.put("ssl.endpoint.identification.algorithm", "");
}
return configs;
}
#Bean
public AdminClient getClient() {
return AdminClient.create(kafkaAdminProperties());
}
}
I expected the below test case run successfully. But i'm getting below error.
java.lang.NullPointerException
at org.kafka.service.TopicService.createTopic(TopicService.java:57)
at org.kafka.service.TopicServiceTest.createTopic(TopicServiceTest.java:100)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
I'm using 2.7.1 spring version with following client dependency.
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.1.1</version>
<scope>test</scope>
<classifier>test</classifier>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>

Property or field 'config' cannot be found on null

I am trying to Implement InitializingBean on my StudentDaoImpl.class and in the function afterPropertiesSet() trying to execute an expression using ExpressionParser, but Spring is saying that property config is not found or is null even if I have declared config in my App.propperties.
AppConfig.class(Configuration Class)
package config;
import dao.StudentDAOImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
#Configuration
#PropertySource("classpath:App.properties")
public class AppConfig {
#Bean("jdbcTemplete")
public JdbcTemplate jdbcTemplate() {
JdbcTemplate template = new JdbcTemplate();
template.setDataSource(dataSource());
return template;
}
#Bean("dataSource")
public DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/springjdbc");
ds.setUsername("root");
ds.setPassword("root");
return ds;
}
#Bean("StudentDaoImpl")
public StudentDAOImpl studentDaoImpl() {
StudentDAOImpl studentdao = new StudentDAOImpl(jdbcTemplate());
return studentdao;
}
#Bean("getProperties")
public static PropertySourcesPlaceholderConfigurer getProperties() {
return new PropertySourcesPlaceholderConfigurer();
}
}
StudentDaoImpl.class (I have removed methods of StudentDao Interface from the code below for you to see properly)
package dao;
import entities.Student;
import mapper.StudentMapper;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import java.util.List;
public class StudentDAOImpl implements StudentDAO, InitializingBean {
private JdbcTemplate jdbcTemplate;
#Value("${config}")
String s;
public StudentDAOImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
#Override
public void afterPropertiesSet() throws Exception {
ExpressionParser expressionParser = new SpelExpressionParser();
System.out.println(s);
Expression expression = expressionParser.parseExpression("config");
String result = (String)expression.getValue();
System.out.println(result);
// if(result == true) {
// System.out.println("[Configuration]: Java Configuration");
// } else {
// System.out.println("[Configuration]: XML Configuration");
// }
}
}
App.properties
config=java
Also, I have seen that I was able to access the property from App.properties using #Value but not with ExpressionParser, Please help me solve this issue.
?
You are parsing a String, not the field; use
Expression expression = expressionParser.parseExpression(config);
to parse an expression in field config - however the value java is not a valid expression.
It is not at all clear what you are trying to do.

Spring boot + MyBatis, multiple datasources and mappers (java and xml), getting "Invalid bound statement (not found)" error

I am trying to create a maven project to access Oracle database with more than one datasource configurations. Here is my code:
First DataSource Config:
package com.business.data.datasource;
import javax.sql.DataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
#Configuration
public class FirstDbConfig {
#Primary
#Bean(name = "firstDataSourceProperties")
#ConfigurationProperties("first.datasource")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
#Primary
#Bean(name = "firstDataSource")
#ConfigurationProperties(prefix = "first.datasource")
public DataSource dataSource(#Qualifier("firstDataSourceProperties") DataSourceProperties properties) {
return DataSourceBuilder.create().build();
}
#Bean(name = "firstSessionFactory")
#Primary
public SqlSessionFactoryBean firstSessionFactory(#Qualifier("firstDataSource") final DataSource firstDataSource)
throws Exception {
final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(firstDataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:./mapper/FirstDbMapper.xml"));
sqlSessionFactoryBean.setTypeAliasesPackage("com.business.data.model");
return sqlSessionFactoryBean;
}
}
Second DataSource Config:
package com.business.data.datasource;
import javax.sql.DataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
#Configuration
public class SecondDbConfig {
#Primary
#Bean(name = "secondDataSourceProperties")
#ConfigurationProperties("second.datasource")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
#Bean(name = "secondDataSource")
#ConfigurationProperties(prefix = "second.datasource")
public DataSource dataSource(#Qualifier("secondDataSourceProperties") DataSourceProperties properties) {
return DataSourceBuilder.create().build();
}
#Bean(name = "secondSessionFactory")
public SqlSessionFactoryBean secondSessionFactory(#Qualifier("secondDataSource") final DataSource firstDataSource)
throws Exception {
final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(firstDataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:./mapper/SecondDbMapper.xml"));
sqlSessionFactoryBean.setTypeAliasesPackage("com.business.data.model");
return sqlSessionFactoryBean;
}
}
First Mapper interface:
package com.business.data.repository;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.business.data.model.PersonDetail;
import org.springframework.stereotype.Repository;
#Repository
#Mapper
public interface FirstDbMapper {
public List<PersonDetail> getUserData(#Param("firstName") String firstName, #Param("lastName") String lastName);
}
Second Mapper interface:
package com.business.data.repository;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.business.data.model.PersonDetail;
import org.springframework.stereotype.Repository;
#Repository
#Mapper
public interface SecondDbMapper {
public List<PersonDetail> getStaffData(#Param("firstName") String firstName, #Param("lastName") String lastName);
}
src/main/resources/mapper/FirstMapper.xml
<?xml version = "1.0" encoding = "UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace must indicate mapper interface full package path . It is an alias here-->
<mapper namespace = "com.business.data.repository.FirstDbMapper">
<select id = "getUserData" parameterType = "map" resultMap = "personDetailMap">
SELECT *
FROM user
WHERE UPPER(first_name) LIKE UPPER(#{firstName}||'%')
AND UPPER(last_name) LIKE UPPER(#{lastName}||'%')
</select>
...
</mapper>
src/main/resources/mapper/SecondMapper.xml
<?xml version = "1.0" encoding = "UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace must indicate mapper interface full package path . It is an alias here-->
<mapper namespace = "com.business.data.repository.SecondDbMapper">
<select id = "getStaffData" parameterType = "map" resultMap = "personDetailMap">
SELECT *
FROM staff
WHERE UPPER(first_name) LIKE UPPER(#{firstName}||'%')
AND UPPER(last_name) LIKE UPPER(#{lastName}||'%')
</select>
...
</mapper>
Service class is like
#Autowired
FirstDbMapper firstDbMapper;
public List<PersonDetail> getUser(String fName, String lName) throws MyServiceException {
...
try {
userList = firstDbMapper.getUserData(fName, lName);
} catch (Exception e) {
...
}
return userList;
}
application.properties:
first.datasource.jdbc-url=jdbc:oracle:thin:#host01:1561:db1
first.datasource.username=user1
first.datasource.password=password1
second.datasource.jdbc-url=jdbc:oracle:thin:#host02:1561:db2
second.datasource.username=user2
second.datasource.password=password2
I also have #MapperScan("com.business.data.repository") in my spring boot application java.
I can only make one datasource work, which is the one with #Primary annotation. I swapped #Primary between the two configuration, always the one with #Primary worked, the other got "Invalid bound statement (not found)" error.
Can anyone help me out?
Thanks!
You can use the #MapperScan annotation on a configuration class to attach mappers to the different session factories. I find it convenient to place mappers in different packages like this:
#MapperScan(basePackages="mapper.package1", sqlSessionFactoryRef="firstSessionFactory")
#MapperScan(basePackages="mapper.package2", sqlSessionFactoryRef="secondSessionFactory")
The issue was resolved by using HikariDataSource.
#Primary
#Bean(name = FIRST_DATASOURCE)
#ConfigurationProperties(prefix = "first.datasource")
public DataSource dataSourceFirst() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}

Spring boot + redis

I am working demo Spring boot application with integration of Redis.
I have referred various site reference but lastly I preferred to follow this: http://www.baeldung.com/spring-data-redis-tutorial
My code is almost same as given in above link. Only change is that I have autowired StudentRepository in my RestController class.
Now when I try to do maven-install at that time it gives me error that
java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'studentController': Unsatisfied dependency expressed through field 'studentRepository'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'studentRepositoryImpl' defined in file [/home/klevu/work/Nimesh/Spring Boot Workspace/bootDemo/target/classes/com/example/demo/redis/repository/StudentRepositoryImpl.class]: Initialization of bean failed; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class com.example.demo.redis.repository.StudentRepositoryImpl]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: No visible constructors in class com.example.demo.redis.repository.StudentRepositoryImpl
When I tried to keep constructor public, it creates build successfully. But I don't know I should do it or not here. I was thinking that rather than Autowiring constructor, I should be able to do setter injection. I also tried below:
#Autowired
private RedisTemplate<String, Student> redisTemplate;
But it is also not working.
package com.example.demo.redis.repository;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;
import com.example.demo.redis.bean.Student;
#Repository
public class StudentRepositoryImpl implements StudentRepository {
private static final String KEY = "Student";
//#Autowired
private RedisTemplate<String, Student> redisTemplate;
private HashOperations<String, String, Student> hashOps;
#Autowired
private StudentRepositoryImpl(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
#PostConstruct
private void init() {
hashOps = redisTemplate.opsForHash();
}
#Override
public void saveStudent(Student person) {
hashOps.put(KEY, person.getId(), person);
}
#Override
public void updateStudent(Student person) {
hashOps.put(KEY, person.getId(), person);
}
#Override
public Student findStudent(String id) {
return hashOps.get(KEY, id);
}
#Override
public Map<String, Student> findAllStudents() {
return hashOps.entries(KEY);
}
#Override
public void deleteStudent(String id) {
hashOps.delete(KEY, id);
}
}
RedisConfiguration are default and code as below:
package com.example.demo.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
#Configuration
public class RedisConfiguration {
#Bean
JedisConnectionFactory jedisConnectionFactory() {
return new JedisConnectionFactory();
}
#Bean
public RedisTemplate<String, Object> redisTemplate(){
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(jedisConnectionFactory());
return template;
}
}
Spring boot main entry point is declared as below:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
#SpringBootApplication
#EnableMongoRepositories(basePackages = {"com.example.demo.mongo.repository"} )
#EnableRedisRepositories(basePackages = {"com.example.demo.redis.repository"})
public class BootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(BootDemoApplication.class, args);
}
}
Demo controller to test redis is as below:
package com.example.demo.controller;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.redis.bean.Student;
import com.example.demo.redis.repository.StudentRepository;
#RestController
#RequestMapping("/student")
public class StudentController {
#Autowired
private StudentRepository studentRepository;
#GetMapping
public ResponseEntity<Map<String, Student>> index() {
Map<String, Student> students = studentRepository.findAllStudents();
return new ResponseEntity<Map<String, Student>>(students, HttpStatus.OK);
}
#RequestMapping(method = RequestMethod.GET, value = "/{id}")
public ResponseEntity<Student> getStudentById(#PathVariable("id") String id) {
Student student = studentRepository.findStudent(id);
return new ResponseEntity<Student>(student, HttpStatus.OK);
}
#RequestMapping(method = RequestMethod.POST)
public ResponseEntity<Student> saveStudent(#RequestBody Student student) {
studentRepository.saveStudent(student);
return new ResponseEntity<Student>(student, HttpStatus.CREATED);
}
#RequestMapping(method = RequestMethod.PUT, value = "/{id}")
public ResponseEntity<Student> updateStudent(#RequestBody Student student) {
studentRepository.updateStudent(student);
return new ResponseEntity<Student>(student, HttpStatus.OK);
}
#RequestMapping(method = RequestMethod.DELETE, value = "/{id}")
public ResponseEntity<Student> deleteMessage(#PathVariable("id") String id) {
studentRepository.deleteStudent(id);
return new ResponseEntity<Student>(HttpStatus.OK);
}
}
You set the constructor as private... change it to public
#Autowired
public StudentRepositoryImpl(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
Change the following redis configuration:
Your original:
#Bean
public RedisTemplate<String, Object> redisTemplate() {
...
}
Change it to:
#Bean
public RedisTemplate<String, ?> redisTemplate(){
...
}
It should work now for you.
You could use Spring Data Redis
Add dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Enable Caching
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
#SpringBootApplication
#EnableCaching
public class RedisDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RedisDemoApplication.class, args);
}
}
Add Cacheable annotation at method to cache in redis
#Cacheable(value = "employee", key = "#id")
public Employee getEmployee(Integer id) {
log.info("Get Employee By Id: {}", id);
Optional<Employee> employeeOptional = employeeRepository.findById(id);
if (!employeeOptional.isPresent()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Id Not foud");
}
return employeeOptional.get();
}

Spring Data ElasticSearch NullPointerException

I have just started with Spring Data ElasticSearch. I have implemented my own repository, but I get a null pointer exception if I try to save an entity. I have got the following code, this is only some test code.
package org.test.elasticsearch.models;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
#Document(indexName = "test", type = "book", shards = 1, replicas = 0)
public class Book {
#Id
private String id;
private String title;
private String author;
public Book(final String id, final String title, final String author) {
this.id = id;
this.title = title;
this.author = author;
}
public String getId() {
return this.id;
}
public void setId(final String id) {
this.id = id;
}
public String getTitle() {
return this.title;
}
public void setTitle(final String title) {
this.title = title;
}
public String getAuthor() {
return this.author;
}
public void setAuthor(final String author) {
this.author = author;
}
}
package org.test.elasticsearch.configs;
import org.elasticsearch.node.NodeBuilder;
import org.test.elasticsearch.repositories.implementations.BookRepositoryImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
#Configuration
#EnableElasticsearchRepositories("org.test.elasticsearch.repositories")
public class ElasticsearchConfiguration {
#Bean
public ElasticsearchOperations elasticsearchTemplate() {
final NodeBuilder nodeBuilder = new NodeBuilder();
return new ElasticsearchTemplate(nodeBuilder.local(true).clusterName("elasticsearch").node().client());
}
#Bean
public BookRepositoryImpl bookRepositoryImplementation() {
return new BookRepositoryImpl();
}
}
package org.test.elasticsearch.repositories;
import org.test.elasticsearch.models.Book;
import org.springframework.data.elasticsearch.repository.ElasticsearchCrudRepository;
public interface BookRepository extends ElasticsearchCrudRepository<Book, String>, BookRepositoryCustom {
// query methods
}
package org.test.webapp;
import org.test.elasticsearch.models.Book;
import org.test.elasticsearch.models.Book.BookBuilder;
import org.test.elasticsearch.repositories.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
#Configuration
#ComponentScan
#EnableAutoConfiguration
public class Test {
#Autowired
static BookRepository BookRepository;
public static void main(final String[] args) {
SpringApplication.run(test.class, args);
final Book testBook = new Book("12345", "TestTitle", "TestAuthor");
BookRepository.save(testBook);
}
}
That's my code. And I get the following message after running my spring boot application.
Exception in thread "main" java.lang.NullPointerException
at org.test.webapp.test.main(Test.java:24)
Does anyone have an idea? And another question: When should I use ElasticsearchTemplate with IndexQuery over a custom repository to save my entities?
Your problem is, actually, with Spring Boot: you are not using it properly. Your Test class should look like this (no "static" access to BookRepository and done differently):
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.test.elasticsearch.models.Book;
import org.test.elasticsearch.repositories.BookRepository;
#Configuration
#ComponentScan(basePackages = {"org.test.elasticsearch.configs", "org.test.webapp"})
#EnableAutoConfiguration
public class Test implements CommandLineRunner {
#Autowired
private BookRepository bookRepository;
#Override
public void run(String... args) {
final Book testBook = new Book("12345", "TestTitle", "TestAuthor");
bookRepository.save(testBook);
}
public static void main(final String[] args) {
SpringApplication.run(Test.class, args);
}
}
Because, as I see it, you don't want to use a web application, so further you only need these dependencies in pom.xml. Nothing related to spring-boot-web:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>1.0.0.RELEASE</version>
</dependency>

Resources