Jooq not working with spring transactions - java

I tried setting up using Jooq with Spring JDBC, everything is working properly except transactions.
This is my current setup:
#Configuration
public class DALConfig {
#Value("${jdbcUrl}")
String jdbcUrl;
#Value("${username}")
String username;
#Value("${password}")
String password;
#Bean(destroyMethod = "close")
DataSource getDataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setUrl(jdbcUrl);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
return dataSource;
}
#Bean(name="transactionManager")
DataSourceTransactionManager getDataSourceTransactionManager() {
return new DataSourceTransactionManager(getDataSource());
}
#Bean(name="transactionAwareDataSource")
TransactionAwareDataSourceProxy getTransactionAwareDataSourceProxy() {
return new TransactionAwareDataSourceProxy(getDataSource());
}
#Bean(name="connectionProvider")
DataSourceConnectionProvider getDataSourceConnectionProvider() {
return new DataSourceConnectionProvider(getTransactionAwareDataSourceProxy());
}
#Bean
DefaultDSLContext getDefaultDSLContext() {
return new DefaultDSLContext(getConfiguration());
}
#Bean
DefaultConfiguration getConfiguration() {
DefaultConfiguration config = new DefaultConfiguration();
config.set(SQLDialect.MYSQL);
config.setConnectionProvider(getDataSourceConnectionProvider());
return config;
}
#Bean
CourseDao getCourseDao() {
return new CourseDao(getConfiguration());
}
}
I am using #Transactional(propagation = Propagation.MANDATORY) annotation on the method which inserts a new Course, but I am getting the following exception org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'.
I have read the docs for spring and jooq but I have not been able to figure out what is missing and what to do to resolve this. Can someone point out what am I missing here.

OK, I got the problem the exception which was supposed to rollback the transaction was happening outside the scope of the transaction. If I added #Transactional in the scope which includes the exception, the rollback works properly.
Also the propagation should be Propagation.MANDATORY should be changed to Propagation.REQUIRED (which is the default).

Related

RESTful webservice using Java Streams and Spring Boot

I try to implement a RESTful WebService that is able to stream millions of records directly from database.
I'm using SpringBoot 2.2.5, Hibernate 5 and PostgreSQL 11
According to this post:
https://www.airpair.com/java/posts/spring-streams-memory-efficiency
one step is needed to set the flag "allowResultAccessAfterCompletion" to true.
But how can I do this in Spring Boot?
So far I do not have any SessionFactory, EntityManagerFactory, Datasource, ... configuration in my application. Everything is autoconfigured by SpringBoot.
If I add the proposed configuration below, the application won't start because of missing SessionFactory.
#Configuration
#EnableTransactionManagement
public class DataConfig {
#Autowired #Bean
public PlatformTransactionManager txManager(SessionFactory sf) {
HibernateTransactionManager mgr = new HibernateTransactionManager(sf);
mgr.setAllowResultAccessAfterCompletion(true);
return mgr;
}
... (the rest of your data config, including the LocalSessionFactoryBean) ...
}
If I provide a SessionFactory bean by unwrapping it from EntityManagerFactory, I get another exception:
Unsatisfied dependency expressed through field 'entityManagerFactory'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'getSessionFactory': Requested bean is currently in creation: Is there an unresolvable circular reference?
Does anyone have a working configuration for my setup?
Can't this flag simply be set by some configuration value in application.properties?
Thank you!
First of all you need to decide whether you need to use Hibernate or Spring JPA for your project. Work with the framework and not against it. Using jpa classes are preferred over hibernate classes by most people today.
Since you are using springboot , the best approach is to work with the framework and use spring-boot-starter-data-jpa which will automatically configure all your necessary beans at startup. In that case, you could provide your own custom beans to override parameters as you want.
In the sample code that you provided in the question, you are using Hibernate classes directly , so you will have to manually create all the necessary beans as spring won't work with you for that unless you disable the auto-configurations which might be causing the circular dependency issue for you.
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories("com.sample.spring.repository")
#PropertySource("classpath:database.properties")
public class DataConfig {
private final String PROPERTY_DRIVER = "driver";
private final String PROPERTY_URL = "url";
private final String PROPERTY_USERNAME = "user";
private final String PROPERTY_PASSWORD = "password";
private final String PROPERTY_SHOW_SQL = "hibernate.show_sql";
private final String PROPERTY_DIALECT = "hibernate.dialect";
#Autowired
Environment environment;
#Bean
LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean lfb = new LocalContainerEntityManagerFactoryBean();
lfb.setDataSource(dataSource());
lfb.setPersistenceProviderClass(HibernatePersistence.class);
lfb.setPackagesToScan("com.sample.spring");
lfb.setJpaProperties(hibernateProps());
return lfb;
}
#Bean
DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setUrl(environment.getProperty(PROPERTY_URL));
ds.setUsername(environment.getProperty(PROPERTY_USERNAME));
ds.setPassword(environment.getProperty(PROPERTY_PASSWORD));
ds.setDriverClassName(environment.getProperty(PROPERTY_DRIVER));
return ds;
}
Properties hibernateProps() {
Properties properties = new Properties();
properties.setProperty(PROPERTY_DIALECT, environment.getProperty(PROPERTY_DIALECT));
properties.setProperty(PROPERTY_SHOW_SQL, environment.getProperty(PROPERTY_SHOW_SQL));
return properties;
}
#Bean
JpaTransactionManager transactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
return transactionManager;
}
}
You can very well provide your own the SessionFactory as well in which case spring boot will not create another one for you. Following is excerpt from Bootstrapping Hibernate 5 with Spring article, feel free to tweak it as per your needs
#Configuration
#EnableTransactionManagement
public class HibernateConf {
#Bean
public LocalSessionFactoryBean sessionFactory() {
LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
sessionFactory.setDataSource(dataSource());
sessionFactory.setPackagesToScan(
{"com.baeldung.hibernate.bootstrap.model" });
sessionFactory.setHibernateProperties(hibernateProperties());
return sessionFactory;
}
#Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1");
dataSource.setUsername("sa");
dataSource.setPassword("sa");
return dataSource;
}
#Bean
public PlatformTransactionManager hibernateTransactionManager() {
HibernateTransactionManager transactionManager
= new HibernateTransactionManager();
transactionManager.setAllowResultAccessAfterCompletion(true);
transactionManager.setSessionFactory(sessionFactory().getObject());
return transactionManager;
}
private final Properties hibernateProperties() {
Properties hibernateProperties = new Properties();
hibernateProperties.setProperty(
"hibernate.hbm2ddl.auto", "create-drop");
hibernateProperties.setProperty(
"hibernate.dialect", "org.hibernate.dialect.H2Dialect");
return hibernateProperties;
}
}
Another approach is to use BeanPostProcessor if you know that spring boot is already creating a HibernateTransactionManager in it's own lifecycle. Following is what the outline of the this BeanPostProcessor would look like
public class HTMPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof HibernateTransactionManager) {
((HibernateTransactionManager)bean).setAllowResultAccessAfterCompletion(true);
}
return bean; // you can return any other object as well
}
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean; // you can return any other object as well
}
}
Hope this helps!!

How to use autowired repositories in Spring Batch integration test?

I am facing some issues while writing integration tests for Spring Batch jobs. The main problem is that an exception is thrown whenever a transaction is started inside the batch job.
Well, first things first. Imagine this is the step of a simple job. A Tasklet for the sake of simplicity. Of course, it is used in a proper batch config (MyBatchConfig) which I also omit for brevity.
#Component
public class SimpleTask implements Tasklet {
private final MyRepository myRepository;
public SimpleTask(MyRepository myRepository) {
this.myRepository = myRepository;
}
#Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
myRepository.deleteAll(); // or maybe saveAll() or some other #Transactional method
return RepeatStatus.FINISHED;
}
}
MyRepository is a very unspecial CrudRepository.
Now, to test that job I use the following test class.
#SpringBatchTest
#EnableAutoConfiguration
#SpringJUnitConfig(classes = {
H2DataSourceConfig.class, // <-- this is a configuration bean for an in-memory testing database
MyBatchConfig.class
})
public class MyBatchJobTest {
#Autowired
private JobLauncherTestUtils jobLauncherTestUtils;
#Autowired
private JobRepositoryTestUtils jobRepositoryTestUtils;
#Autowired
private MyRepository myRepository;
#Test
public void testJob() throws Exception {
var testItems = List.of(
new MyTestItem(1),
new MyTestItem(2),
new MyTestItem(3)
);
myRepository.saveAll(testItems); // <--- works perfectly well
jobLauncherTestUtils.launchJob();
}
}
When it comes to the tasklet execution and more precisely to the deleteAll() method call this exception is fired:
org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction; nested exception is java.lang.IllegalStateException: Already value [org.springframework.jdbc.datasource.ConnectionHolder#68f48807] for key [org.springframework.jdbc.datasource.DriverManagerDataSource#49a6f486] bound to thread [SimpleAsyncTaskExecutor-1]
at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:448)
...
Do you have any ideas why this is happening?
As a workaround I currently mock the repository with #MockBean and back it with an ArrayList but this is not what the inventor intended, I guess.
Any advice?
Kind regards
Update 1.1 (includes solution)
The mentioned data source configuration class is
#Configuration
#EnableJpaRepositories(
basePackages = {"my.project.persistence.repository"},
entityManagerFactoryRef = "myTestEntityManagerFactory",
transactionManagerRef = "myTestTransactionManager"
)
#EnableTransactionManagement
public class H2DataSourceConfig {
#Bean
public DataSource myTestDataSource() {
var dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1");
return dataSource;
}
#Bean
public LocalContainerEntityManagerFactoryBean myTestEntityManagerFactory() {
var emFactory = new LocalContainerEntityManagerFactoryBean();
var adapter = new HibernateJpaVendorAdapter();
adapter.setDatabasePlatform("org.hibernate.dialect.H2Dialect");
adapter.setGenerateDdl(true);
emFactory.setDataSource(myTestDataSource());
emFactory.setPackagesToScan("my.project.persistence.model");
emFactory.setJpaVendorAdapter(adapter);
return emFactory;
}
#Bean
public PlatformTransactionManager myTestTransactionManager() {
return new JpaTransactionManager(myTestEntityManagerFactory().getObject());
}
#Bean
public BatchConfigurer testBatchConfigurer() {
return new DefaultBatchConfigurer() {
#Override
public PlatformTransactionManager getTransactionManager() {
return myTestTransactionManager();
}
};
}
}
By default, when you declare a datasource in your application context, Spring Batch will use a DataSourceTransactionManager to drive step transactions, but this transaction manager knows nothing about your JPA context.
If you want to use another transaction manager, you need to override BatchConfigurer#getTransactionManager and return the transaction manager you want to use to drive step transactions. In your case, you are only declaring a transaction manager bean in the application context which is not enough. Here a quick example:
#Bean
public BatchConfigurer batchConfigurer() {
return new DefaultBatchConfigurer() {
#Override
public PlatformTransactionManager getTransactionManager() {
return new JpaTransactionManager(myTestEntityManagerFactory().getObject());
}
};
}
For more details, please refer to the reference documentation.

Why I can't make rollback on in-memory DB?

I have the test class which testing my DAO class. In theory, it should run a chain of each before → test → after in one transaction and make rollback after that, but seemingly it is not. Every time creates a new id (123->456 instead of 123->123). I guess that in-memory DBs (I use H2) works this way, and I was not mistaken. With Postgres setup, it works good enough.
I've checked:
configurations, annotations, and propagation levels
I tried to use hibernate.connection.autocommit = false
HSQLDB
But I didn't find a mistake there.
TransactionSynchronizationManager.isActualTransactionActive() returns true.
PersistenceConfig:
#Configuration
#ComponentScan("com.beginnercourse.softcomputer")
#PropertySource({"classpath:persistence-postgres.properties"})
#PropertySource({"classpath:persistence-h2.properties"})
#EnableTransactionManagement
public class PersistenceConfig {
#Autowired
private Environment environment;
#Bean
#Autowired
public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) {
HibernateTransactionManager transactionManager = new HibernateTransactionManager();
transactionManager.setSessionFactory(sessionFactory);
return transactionManager;
}
#Bean
#Profile("postgres")
public DataSource postgresDataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(requireNonNull(environment.getProperty("jdbc.postgres.driverClassName")));
dataSource.setUrl(requireNonNull(environment.getProperty("jdbc.postgres.connection_url")));
dataSource.setUsername(requireNonNull(environment.getProperty("jdbc.postgres.username")));
dataSource.setPassword(requireNonNull(environment.getProperty("jdbc.postgres.password")));
return dataSource;
}
#Bean
#Profile("postgres")
public LocalSessionFactoryBean postgresSessionFactory() {
LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
sessionFactory.setDataSource(postgresDataSource());
sessionFactory.setPackagesToScan(
new String[]{"com.beginnercourse.softcomputer"});
sessionFactory.setHibernateProperties(postgresAdditionalProperties());
return sessionFactory;
}
private Properties postgresAdditionalProperties() {
final Properties hibernateProperties = new Properties();
hibernateProperties.setProperty("hibernate.hbm2ddl.auto", requireNonNull(environment.getProperty("hibernate.postgres.hbm2ddl.auto")));
hibernateProperties.setProperty("hibernate.dialect", requireNonNull(environment.getProperty("hibernate.postgres.dialect")));
hibernateProperties.setProperty("hibernate.show_sql", requireNonNull(environment.getProperty("hibernate.postgres.show_sql")));
hibernateProperties.setProperty("hibernate.default_schema", requireNonNull(environment.getProperty("hibernate.postgres.default_schema")));
// hibernateProperties.setProperty("hibernate.cache.use_second_level_cache", requireNonNull(environment.getProperty("hibernate.cache.use_second_level_cache")));
// hibernateProperties.setProperty("hibernate.cache.use_query_cache", requireNonNull(environment.getProperty("hibernate.cache.use_query_cache")));
return hibernateProperties;
}
#Bean
#Profile("oracle")
public DataSource oracleDataSource() throws NamingException {
return (DataSource) new JndiTemplate().lookup(requireNonNull(environment.getProperty("jdbc.url")));
}
#Bean
#Profile("test")
public LocalSessionFactoryBean testSessionFactory(DataSource dataSource ) {
LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
// postgresSessionFactory.setDataSource(postgresDataSource());
sessionFactory.setDataSource(dataSource);
sessionFactory.setPackagesToScan(
new String[]{"com.beginnercourse.softcomputer"});
sessionFactory.setHibernateProperties(testAdditionalProperties());
return sessionFactory;
}
#Bean
#Profile("test")
public DataSource h2DataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(requireNonNull(environment.getProperty("jdbc.h2.driverClassName")));
dataSource.setUrl(requireNonNull(environment.getProperty("jdbc.h2.connection_url")));
dataSource.setUsername(requireNonNull(environment.getProperty("jdbc.h2.username")));
dataSource.setPassword(requireNonNull(environment.getProperty("jdbc.h2.password")));
return dataSource;
}
private Properties testAdditionalProperties() {
final Properties hibernateProperties = new Properties();
hibernateProperties.setProperty("hibernate.hbm2ddl.auto", requireNonNull(environment.getProperty("hibernate.h2.hbm2ddl.auto")));
hibernateProperties.setProperty("hibernate.dialect", requireNonNull(environment.getProperty("hibernate.h2.dialect")));
hibernateProperties.setProperty("hibernate.show_sql", requireNonNull(environment.getProperty("hibernate.h2.show_sql")));
hibernateProperties.setProperty("hibernate.globally_quoted_identifiers", requireNonNull(environment.getProperty("hibernate.h2.globally_quoted_identifiers")));
hibernateProperties.setProperty("hibernate.connection.autocommit", requireNonNull(environment.getProperty("hibernate.h2.connection.autocommit")));
return hibernateProperties;
}
}
H2 Properties
jdbc.h2.driverClassName=org.h2.Driver
jdbc.h2.connection_url=jdbc:h2:mem:e-commerce
jdbc.h2.username=sa
jdbc.h2.password=sa
hibernate.h2.dialect=org.hibernate.dialect.H2Dialect
hibernate.h2.show_sql=false
hibernate.h2.hbm2ddl.auto=update
hibernate.h2.globally_quoted_identifiers=true
hibernate.h2.connection.autocommit = false
TestDaoImpl
#ActiveProfiles(profiles = "test")
//#ActiveProfiles(profiles = "postgres")
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = {
WebConfig.class,
PersistenceConfig.class,
})
#WebAppConfiguration
#Transactional
#Rollback
public class CustomerDaoImplTest {
#Autowired
private CustomerDao customerDao;
#Before
public void setUp() throws Exception {
CustomerEntity veronicaCustomer = new CustomerEntity();
veronicaCustomer.setName("Veronica");
customerDao.create(veronicaCustomer);
CustomerEntity hannaCustomer = new CustomerEntity();
hannaCustomer.setName("Hanna");
customerDao.create(hannaCustomer);
CustomerEntity ericCustomer = new CustomerEntity();
ericCustomer.setName("Eric");
customerDao.create(ericCustomer);
}
#After
public void tearDown() throws Exception {
customerDao.remove((long) 1);
customerDao.remove((long) 2);
customerDao.remove((long) 3);
}
#Test
public void find_must_return_an_object_by_id() throws NoCustomerWithSuchParametersException {
CustomerEntity customer = new CustomerEntity();
customer.setName("Veronica");
assertEquals(customerDao.find((long) 1).get().getName(), customer.getName());
}
#Test(expected = EntityNotFoundException.class)
public void should_optional_empty() {
assertEquals(customerDao.find((long) 55), Optional.empty());
}
}
Has anyone else come across something similar?
What makes you think the transaction is not being rolled back?
Ids with values 1,2,3 were allocated and, despite the rollback, the H2 database has simply declined to reuse them.
There's a discussion on that here (in terms of MySQL but similar behaviour) MySQL AUTO_INCREMENT does not ROLLBACK.
You could reset the auto-increment value between tests:
Resetting autoincrement in h2
or you could simply update your code to manually set the identifiers:
CustomerEntity veronicaCustomer = new CustomerEntity();
veronicaCustomer.setid(1L);
veronicaCustomer.setName("Veronica");
According to this question (H2 equivalent to SET IDENTITY_INSERT ) that should work in H2 without any issues. With other databases (SQLServer for example ) you may need to explicitly enable identity inserts to manually set a value on an identity column.

Spring JPA: How to update 2 different tables in 2 different `DataSource` in the same request?

In our application, we have a common database called central and every customer will have their own database with exactly the same set of tables. Each customer's database might be hosted on our own server or the customer's server based on the requirement of the customer organization.
To handle this multi-tenant requirement, we're extending the AbstractRoutingDataSource from Spring JPA and overriding the determineTargetDataSource() method to create a new DataSource and establish a new connection on the fly based on the incoming customerCode. We also use a simple DatabaseContextHolder class to store the current datasource context in a ThreadLocal variable. Our solution is similar to what is describe in this article.
Let's say in a single request, we'll need to update some data in both the central database and the customer's database as following.
public void createNewEmployeeAccount(EmployeeData employee) {
DatabaseContextHolder.setDatabaseContext("central");
// Code to save a user account for logging in to the system in the central database
DatabaseContextHolder.setDatabaseContext(employee.getCustomerCode());
// Code to save user details like Name, Designation, etc. in the customer's database
}
This code would only work if determineTargetDataSource() is called every time just before any SQL queries gets executed so that we can switch the DataSource dynamically half way through our method.
However, from this Stackoverflow question, it seems like determineTargetDataSource() is only called once for each HttpRequest when a DataSource is being retrieved for the very first time in that request.
I'd be very grateful if you can give me some insights into when AbstractRoutingDataSource.determineTargetDataSource() actually gets called. Besides, if you've dealt with a similar multi-tenant scenario before, I'd love to hear your opinion on how I should deal with the updating of multiple DataSource in a single request.
We found a working solution, which is a mix of static data source settings for our central database and dynamic data source settings for our customer's database.
In essence, we know exactly which table comes from which database. Hence, we were able to separate our #Entity classes into 2 different packages as following.
com.ft.model
-- central
-- UserAccount.java
-- UserAccountRepo.java
-- customer
-- UserProfile.java
-- UserProfileRepo.java
Subsequently, we created two #Configuration classes to set up the data source setting for each package. For our central database, we use static settings as following.
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = "entityManagerFactory",
transactionManagerRef = "transactionManager",
basePackages = { "com.ft.model.central" }
)
public class CentralDatabaseConfiguration {
#Primary
#Bean(name = "dataSource")
public DataSource dataSource() {
return DataSourceBuilder.create(this.getClass().getClassLoader())
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url("jdbc:sqlserver://localhost;databaseName=central")
.username("sa")
.password("mhsatuck")
.build();
}
#Primary
#Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, #Qualifier("dataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.ft.model.central")
.persistenceUnit("central")
.build();
}
#Primary
#Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager (#Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
For the #Entity in the customer package, we set up dynamic data source resolver using the following #Configuration.
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = "customerEntityManagerFactory",
transactionManagerRef = "customerTransactionManager",
basePackages = { "com.ft.model.customer" }
)
public class CustomerDatabaseConfiguration {
#Bean(name = "customerDataSource")
public DataSource dataSource() {
return new MultitenantDataSourceResolver();
}
#Bean(name = "customerEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, #Qualifier("customerDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.ft.model.customer")
.persistenceUnit("customer")
.build();
}
#Bean(name = "customerTransactionManager")
public PlatformTransactionManager transactionManager(#Qualifier("customerEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
In the MultitenantDataSourceResolver class, we plan to maintain a Map of the created DataSource using customerCode as key. From each incoming request, we will get the customerCode and inject it into our MultitenantDataSourceResolver to get the correct DataSource within the determineTargetDataSource() method.
public class MultitenantDataSourceResolver extends AbstractRoutingDataSource {
#Autowired
private Provider<CustomerWrapper> customerWrapper;
private static final Map<String, DataSource> dsCache = new HashMap<String, DataSource>();
#Override
protected Object determineCurrentLookupKey() {
try {
return customerWrapper.get().getCustomerCode();
} catch (Exception ex) {
return null;
}
}
#Override
protected DataSource determineTargetDataSource() {
String customerCode = (String) this.determineCurrentLookupKey();
if (customerCode == null)
return MultitenantDataSourceResolver.getDefaultDataSource();
else {
DataSource dataSource = dsCache.get(customerCode);
if (dataSource == null)
dataSource = this.buildDataSourceForCustomer();
return dataSource;
}
}
private synchronized DataSource buildDataSourceForCustomer() {
CustomerWrapper wrapper = customerWrapper.get();
if (dsCache.containsKey(wrapper.getCustomerCode()))
return dsCache.get(wrapper.getCustomerCode() );
else {
DataSource dataSource = DataSourceBuilder.create(MultitenantDataSourceResolver.class.getClassLoader())
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url(wrapper.getJdbcUrl())
.username(wrapper.getDbUsername())
.password(wrapper.getDbPassword())
.build();
dsCache.put(wrapper.getCustomerCode(), dataSource);
return dataSource;
}
}
private static DataSource getDefaultDataSource() {
return DataSourceBuilder.create(CustomerDatabaseConfiguration.class.getClassLoader())
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url("jdbc:sqlserver://localhost;databaseName=central")
.username("sa")
.password("mhsatuck")
.build();
}
}
The CustomerWrapper is a #RequestScope object whose values will be populated on each request by the #Controller. We use java.inject.Provider to inject it into our MultitenantDataSourceResolver.
Lastly, even though, logically, we will never save anything using the default DataSource because all requests will always contain a customerCode, at startup time, there is no customerCode available. Hence, we still need to provide a valid default DataSource. Otherwise, the application will not be able to start.
If you have any comments or a better solution, please let me know.

Configure transaction in Spring 4.1.5 without XML

I'm writing application which connects with Oracle Database. I call function from DB which inserts new records to table. And after this callback I can decide what I want to do: commit or rollback.
Unfortunalety I'm new in Spring, so I have problems with configuration. And what's more I want to make this configuration in Java class, not in XML. And here I need your help.
UPDATED CODE:
ApplicationConfig code:
#Configuration
#EnableTransactionManagement
#ComponentScan("hr")
#PropertySource({"classpath:jdbc.properties", "classpath:functions.properties", "classpath:procedures.properties"})
public class ApplicationConfig {
#Autowired
private Environment env;
#Bean(name="dataSource")
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(env.getProperty("jdbc.driver"));
dataSource.setUrl(env.getProperty("jdbc.url"));
dataSource.setUsername(env.getProperty("jdbc.username"));
dataSource.setPassword(env.getProperty("jdbc.password"));
dataSource.setDefaultAutoCommit(false);
return dataSource;
}
#Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
return jdbcTemplate;
}
#Bean(name="txName")
public PlatformTransactionManager txManager() {
DataSourceTransactionManager txManager = new DataSourceTransactionManager();
txManager.setDataSource(dataSource());
return txManager;
}
}
I have Dao and Service, where both implements proper interface.
Service implementation:
#Service
public class HumanResourcesServiceImpl implements HumanResourcesService {
#Autowired
private HumanResourcesDao hrDao;
#Override
public String generateData(int rowsNumber) {
return hrDao.generateData(rowsNumber);
}
#Override
#Transactional("txName")
public void shouldCommit(boolean doCommit, Connection connection) throws SQLException {
hrDao.shouldCommit(doCommit, connection);
}
}
Dao implementation:
#Repository
public class HumanResourcesDaoImpl implements HumanResourcesDao {
private JdbcTemplate jdbcTemplate;
private SimpleJdbcCall generateData;
#Autowired
public HumanResourcesDaoImpl(JdbcTemplate jdbcTemplate, Environment env) {
this.jdbcTemplate = jdbcTemplate;
generateData = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName(env.getProperty("procedure.generateData"));
}
#Override
public String generateData(int rowsNumber) {
HashMap<String, Object> params = new HashMap<>();
params.put("i_rowsNumber", rowsNumber);
Map<String, Object> m = generateData.execute(params);
return (String) m.get("o_execution_time");
}
#Override
#Transactional("txName")
public void shouldCommit(boolean doCommit, Connection connection) throws SQLException {
if(doCommit) {
connection.commit();
} else {
connection.rollback();
}
}
}
Main class code:
public class Main extends Application implements Initializable {
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
hrService = context.getBean(HumanResourcesService.class);
BasicDataSource ds = (BasicDataSource)context.getBean("dataSource");
Connection connection = ds.getConnection();
//do something and call
//hrService.generateData
//do something and call
//hrService.shouldCommit(true, connection);
//which commit or rollback generated data from previoues callback
}
}
UPDATE:
I think that the problem is with connection, because this statement:
this.jdbcTemplate.getDataSource().getConnection();
creates new connection, so then there is nothing to commit or rollback. But still I can't figure why this doesn't work properly. No errors, no new records...
What is wierd, is that when I debuged connection.commit(); I found out that in DelegatingConnection.java, parameter this has proper connection, but there is something like:
_conn.commit();
and _conn has different connection. Why?
Should I in some way synchronize connection for those 2 methods or what? Or this is only one connection? To be honest, I'm not sure how it works exactly. One connection and all callbacks to stored procedures are in this connection or maybe with each callback new connection is created?
Real question is how to commit or rollback data from previous callback which do insert into table?
One easy way to do this is to annotate the method with #Transactional:
#Transactional
public void myBeanMethod() {
...
if (!doCommit)
throw new IllegalStateException(); // any unchecked will do
}
and spring will roll all database changes back.
Remember to add #EnableTransactionManagement to your spring application/main class
You can use #Transactional and #EnableTransactionManagement to setup transactions without using the XML configuration. In short, annotate the methods/classes you want to have transactions with #Transactional. To setup the transactional management you use the #EnableTransactionManagement inside your #Configuration.
See Springs docs for example on how to use both. The #EnableTransactionManagement is detailed in the JavaDocs but should match the XML configuration.
UPDATE
The problem is that you are mixing raw JDBC calls (java.sql.Connection) with Spring JDBC. When you execute your SimpleJdbcCall, Spring creates a new Connection. This is not the same Connection as the one you later try to commit. Hence, nothing happens when you perform the commit. I tried to somehow get the connection that the SimpleJdbcCall uses, but could not find any easy way.
To test this I tried the following (I did not use params):
#Override
public String generateData(int rowsNumber) {
//HashMap<String, Object> params = new HashMap<>();
//params.put("i_rowsNumber", rowsNumber);
//Map<String, Object> m = generateData.execute(params);
Connection targetConnection = DataSourceUtils.getTargetConnection(generateData.getJdbcTemplate().getDataSource().getConnection());
System.out.println(targetConnection.prepareCall((generateData.getCallString())).execute());
targetConnection.commit();
return (String) m.get("o_execution_time");
}
If I don't save the targetConnection, and instead try to get the connection again by calling DataSourceUtils.getTargetConnection() when committing, nothing happens. Thus, you must commit on the same connection that you perform the statement on. This does not seem to be easy, nor the proper way.
The solution is to drop the java.sql.Connection.commit() call. Instead, you use Spring Transactions completly. If you use #Transaction on the method that performs database call, Spring will automatically commit when the method has finished. If the method body experiences any Exception (even outside the actual database call) it will automatically rollback. In other words, this should suffice for normal Transaction management.
However, if you are doing batch processing, and wish to have more control over your transactions with commits and rollbacks, you can still use Spring. To programatically control transactions with Spring, you can use TransactionTemplate which have commit and rollback methods. Don't have time to give you proper samples, but may do so in later days if you are still stuck ;)
#Configuration
#EnableTransactionManagement
#ComponentScan(basePackages="org.saat")
#PropertySource(value="classpath:resources/db.properties",ignoreResourceNotFound=true)
public class AppConfig {
#Autowired
private Environment env;
#Bean(name="dataSource")
public DataSource getDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("db.driver"));
dataSource.setUrl(env.getProperty("db.url"));
dataSource.setUsername(env.getProperty("db.username"));
dataSource.setPassword(env.getProperty("db.password"));
return dataSource;
}
#Bean(name="entityManagerFactoryBean")
public LocalContainerEntityManagerFactoryBean getSessionFactory() {
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean ();
factoryBean.setDataSource(getDataSource());
factoryBean.setPackagesToScan("org.saat");
factoryBean.setJpaVendorAdapter(getJpaVendorAdapter());
Properties props=new Properties();
props.put("hibernate.dialect", env.getProperty("hibernate.dialect"));
props.put("hibernate.hbm2ddl.auto",env.getProperty("hibernate.hbm2ddl.auto"));
props.put("hibernate.show_sql",env.getProperty("hibernate.show_sql"));
factoryBean.setJpaProperties(props);
return factoryBean;
}
#Bean(name="transactionManager")
public JpaTransactionManager getTransactionManager() {
JpaTransactionManager jpatransactionManager = new JpaTransactionManager();
jpatransactionManager.setEntityManagerFactory(getSessionFactory().getObject());
return jpatransactionManager;
}
#Bean
public JpaVendorAdapter getJpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
return hibernateJpaVendorAdapter;
}
}

Categories

Resources