I'm working on a Spring JPA/Hibernate project which must use a database user's connection for persistence.
In fact, every user has their own Oracle DB connection and I must use it in the app to know who accesses and writes to my database.
How can I use a different db connection with Hibernate instead of using a single app connection (in the applicationContext datasource)?
Read about AbstractRoutingDataSource and my old question
AbstractRoutingDataSource change map in runtime
Session listener:
public class SessionListener implements HttpSessionListener {
#Autowired
DataSourceMap dataSourceMap;
#Override
public void sessionCreated(HttpSessionEvent httpSessionEvent) {
System.out.println("tworzenie sesji");
}
#Override
public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
System.out.println(httpSessionEvent.getSession().getId());
dataSourceMap.removeSource(httpSessionEvent.getSession().getId());
}
}
Routing:
#Component
public class CustomRoutingDataSource extends AbstractRoutingDataSource{
#Autowired
DataSourceMap dataSources;
#Override
protected Object determineCurrentLookupKey() {
//setDataSources(dataSources);
afterPropertiesSet();
System.out.println("test");
if( SecurityContextHolder.getContext().getAuthentication()!=null) {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
return request.getSession().getId();
}
return "auth";
}
#Autowired
public void setDataSources(DataSourceMap dataSources) {
System.out.println("data adding");
setTargetDataSources(dataSources.getDataSourceMap());
}
}
Map:
#Component
#Scope(value = "singleton")
public class DataSourceMap {
private Map<Object,Object> dataSourceMap;
public DataSourceMap()
{
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.driverClassName("org.h2.Driver");
dataSourceBuilder.url("jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
dataSourceBuilder.username("sa");
dataSourceBuilder.password("");
dataSourceMap=new HashMap<Object,Object>();
dataSourceMap.put("auth",dataSourceBuilder.build());
}
public void addDataSource(String session,DataSource dataSource)
{
this.dataSourceMap.put(session,dataSource);
}
public Map<Object,Object> getDataSourceMap()
{
return dataSourceMap;
}
public void removeSource(String session)
{
dataSourceMap.remove(session);
}
}
Related
I have a code snippet that looks like this one
Service with #Transactional method
public class XService {
private Repo1 repo1;
private Repo2 repo2;
private Repo3 repo3;
XService(Repo1 repo1, Repo2 repo2, Repo3 repo3) {
this.repo1 = repo1;
this.repo2 = repo2;
this.repo3 = repo3;
}
#Transactional(rollbackFor = Exception.class)
public SomeObject method(Arg1 arg1, Arg2 arg2) {
repo1.method1();
repo2.method2();
repo3.method3(); // probability of exception here, in which case rollback is needed
}
}
Class from where method is invoked
public class YService {
private XService xService;
public YService(XService xService) {
this.xService = xService;
}
public SomeObject method(Arg1 arg1, Arg2 arg2) {
xService.method(arg1, arg2);
}
}
I have also added the #EnableTransactionManagement on my SpringBootApplication class. But the database operations from repo1 and repo2 are not rolled back in case of Exception from repo3.
Every repository is using Spring JDBCTemplate for querying the database.
Configuration class
#Configuration
public class ConfigurationClass {
#Bean
#Inject
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
#Bean
#Inject
public DataSource dataSource() {
// setting config properties here
return new HikariDataSource(config);
}
#Bean
#Inject
public Repo1 repo1(JDBCTemplate template) {
return new Repo1(template);
}
#Bean
#Inject
public Repo2 repo2(JDBCTemplate template) {
return new Repo2(template);
}
#Bean
#Inject
public Repo3 repo3(JDBCTemplate template) {
return new Repo3(template);
}
#Bean
#Inject
public XService XService(Repo1 repo1, Repo2 repo2, Repo3 repo3) {
return new XService(repo1, repo2, repo3);
}
#Bean
#Inject
public YService YService(XService xService) {
return new YService(xService);
}
}
Using a TransactionTemplate and enclosing my repo calls in the template worked.
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
#Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws TransactionException {
repo1.method1();
repo2.method2();
repo3.method3();
}
});
I don't know the reason this one worked. It will be great if anyone can help in understanding the reason behind this.
I am currently trying to use jOOQ in my springboot project to insert certain data given by user from a html form into a MySQL db. This is my MainController responsible for getting data from the form and executing the query through a VisitorRepository: I'm using both add and insertVisitor methods but neither of them are working.
#Controller
public class MainController {
private final VisitorRepository visitorRepository;
#Autowired
public MainController (VisitorRepository visitorRepository) {
this.visitorRepository = visitorRepository;
}
#RequestMapping(value = "/", method = RequestMethod.GET)
public String main() {
return "main";
}
#ResponseBody
#PostMapping("/")
public Phonenum firstSubmit(Phonenum phonenum){
System.out.println(phonenum.getPhonenumber());
visitorRepository.insertVisitor(phonenum.getPhonenumber(),"now", phonenum.getName());
return visitorRepository.add(phonenum);
}
}
VisitorRepository interface:
public interface VisitorRepository {
public Phonenum add(Phonenum visitor);
public List<Phonenum> findAll();
public void insertVisitor(String phoneNumber, String submitTime, String name);
}
And the repository implementation:
#Repository
#Transactional
public class VisitorRepo implements VisitorRepository{
private final DSLContext dslContext;
public VisitorRepo(DSLContext dslContext){
this.dslContext = dslContext;
}
public void insertVisitor(String phoneNumber, String submitTime, String name){
this.dslContext
.insertInto(Phonenum.PHONENUM)
.columns(Phonenum.PHONENUM.PHONENUMBER, Phonenum.PHONENUM.SUBMITTIME, Phonenum.PHONENUM.NAME)
.values(phoneNumber, submitTime, name);
}
public com.demo.visitorlog.model.Phonenum add(com.demo.visitorlog.model.Phonenum visitor) {
this.dslContext
.insertInto(Phonenum.PHONENUM)
.columns(Phonenum.PHONENUM.PHONENUMBER, Phonenum.PHONENUM.SUBMITTIME, Phonenum.PHONENUM.NAME)
.values(visitor.getPhonenumber(), String.valueOf("submit time"), visitor.getName());
System.out.println("ran");
return visitor;
}
I'm not entirely sure how to setup the config class but here is what I have:
#Configuration
public class mainConfig{
#Autowired
private DataSource dataSource;
#Bean
public DataSourceConnectionProvider connectionProvider() {
return new DataSourceConnectionProvider
(new TransactionAwareDataSourceProxy(dataSource));
}
#Bean
public DefaultDSLContext dsl() {
return new DefaultDSLContext(configuration());
}
public DefaultConfiguration configuration() {
DefaultConfiguration jooqConfiguration = new DefaultConfiguration();
jooqConfiguration.set(connectionProvider());
return jooqConfiguration;
}
So I get the System.out.println() after the post request but when I look into my db there isn't anything inserted. Am I missing something? Any sort of help would be greatly appreciated :)
You have to call .execute() to execute the query
this.dslContext
.insertInto(Phonenum.PHONENUM)
.columns(Phonenum.PHONENUM.PHONENUMBER, Phonenum.PHONENUM.SUBMITTIME, Phonenum.PHONENUM.NAME)
.values(visitor.getPhonenumber(), String.valueOf("submit time"), visitor.getName())
.execute();
The problem was with Spring Security automatically enabling csrf Protection which I had to manual disable in the Security Config class using:
#Override
protected void configure(HttpSecurity http) throws Exception{
http.cors().and().csrf().disable();
}
I try to make my test to work with Spring #Transactional annotation.
#ContextConfiguration(classes = SomeTest.SomeTestSpringConfig.class)
#RunWith(SpringJUnit4ClassRunner.class)
public class SomeTest {
#Autowired
MyBean some;
#Autowired
PlatformTransactionManager transactionManager;
#Test
public void testSpring() throws Exception {
some.method();
assertTrue(some.isTransactionalWorks);
}
#EnableAspectJAutoProxy(proxyTargetClass = true)
#EnableLoadTimeWeaving
#EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
#TransactionConfiguration
static class SomeTestSpringConfig {
#Bean
PlatformTransactionManager transactionManager() {
return new MyTransactionManager(dataSource());
}
#Bean
MyBean some() {
return new MyBean();
}
#Bean
DataSource dataSource() {
return new SimpleDriverDataSource(Driver.load(), "jdbc:h2:mem:unit-test");
}
}
}
class MyBean {
#Autowired
DataSource dataSource;
public boolean isTransactionalWorks;
#Transactional
private void someInTransaction() {
try {
dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
System.out.println("I should be in transaction");
}
public void method() {
someInTransaction();
}
}
class MyTransactionManager implements PlatformTransactionManager, InitializingBean {
private final DataSourceTransactionManager base = new DataSourceTransactionManager();
#Autowired
MyBean some;
public MyTransactionManager(DataSource datasource) {
base.setDataSource(datasource);
}
#Override
public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
some.isTransactionalWorks = true;
return base.getTransaction(definition);
}
#Override
public void commit(TransactionStatus status) throws TransactionException {
base.commit(status);
}
#Override
public void rollback(TransactionStatus status) throws TransactionException {
base.rollback(status);
}
#Override
public void afterPropertiesSet() throws Exception {
base.afterPropertiesSet();
}
}
Also I added -javaagent:D:/libs/spring-instrument-4.1.7.RELEASE.jar to VM options for this test.
But it always fails. What did I miss?
Please check this link, i think it is the similar problem u are facing.
How to configure AspectJ with Load Time Weaving without Interface
In this link he has asked to provide both aspectjweaver.jar and spring-instrument.jar in vm argument.
Good to know it worked. :)
I'm referring to this article, in which we can use the AbstractRoutingDataSource from Spring Framework to dynamically change the data source used by the application. I'm using Mybatis (3.3.0) with Spring (4.1.6.RELEASE). I want to switch to the backup database if exception occurs while getting data from main db. In this example, i have used hsql and mysql db.
RoutingDataSource:
public class RoutingDataSource extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getTargetDataSource();
}
}
DataSourceContextHolder:
public class DataSourceContextHolder {
private static final ThreadLocal<DataSourceEnum> contextHolder = new ThreadLocal<DataSourceEnum>();
public static void setTargetDataSource(DataSourceEnum targetDataSource) {
contextHolder.set(targetDataSource);
}
public static DataSourceEnum getTargetDataSource() {
return (DataSourceEnum) contextHolder.get();
}
public static void resetDefaultDataSource() {
contextHolder.remove();
}
}
ApplicationDataConfig:
#Configuration
#MapperScan(basePackages = "com.sample.mapper")
#ComponentScan("com.sample.config")
#PropertySource(value = {"classpath:app.properties"},
ignoreResourceNotFound = true)
public class ApplicationDataConfig {
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
PropertySourcesPlaceholderConfigurer configurer =
new PropertySourcesPlaceholderConfigurer();
return configurer;
}
#Bean
public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
RoutingDataSource routingDataSource = new RoutingDataSource();
routingDataSource.setDefaultTargetDataSource(dataSource1());
Map<Object, Object> targetDataSource = new HashMap<Object, Object>();
targetDataSource.put(DataSourceEnum.HSQL, dataSource1());
targetDataSource.put(DataSourceEnum.BACKUP, dataSource2());
routingDataSource.setTargetDataSources(targetDataSource);
sessionFactory.setDataSource(routingDataSource);
sessionFactory.setTypeAliasesPackage("com.sample.common.domain");
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath*:com/sample/mapper/**/*.xml"));
return sessionFactory;
}
#Bean
public DataSource dataSource1() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).addScript(
"classpath:database/app-hsqldb-schema.sql").addScript(
"classpath:database/app-hsqldb-datascript.sql").build();
}
#Bean
public DataSource dataSource2() {
PooledDataSourceFactory pooledDataSourceFactory = new PooledDataSourceFactory();
pooledDataSourceFactory.setProperties(jdbcProperties());
return pooledDataSourceFactory.getDataSource();
}
#Bean
protected Properties jdbcProperties() {
//Get the data from properties file
Properties jdbcProperties = new Properties();
jdbcProperties.setProperty("url", datasourceUrl);
jdbcProperties.setProperty("driver", datasourceDriver);
jdbcProperties.setProperty("username", datasourceUsername);
jdbcProperties.setProperty("password", datasourcePassword);
jdbcProperties.setProperty("poolMaximumIdleConnections", maxConnectionPoolSize);
jdbcProperties.setProperty("poolMaximumActiveConnections", minConnectionPoolSize);
return jdbcProperties;
}
}
Client:
#Autowired
private ApplicationMapper appMapper;
public MyObject getObjectById(String Id) {
MyObject myObj = null;
try{
DataSourceContextHolder.setTargetDataSource(DataSourceEnum.HSQL);
myObj = appMapper.getObjectById(Id);
}catch(Throwable e){
DataSourceContextHolder.setTargetDataSource(DataSourceEnum.BACKUP);
myObj = appMapper.getObjectById(Id);
}finally{
DataSourceContextHolder.resetDefaultDataSource();
}
return getObjectDetails(myObj);
}
I'm getting the following exception
### Error querying database. Cause: java.lang.IllegalArgumentException: DataSource router not initialized
However i'm able to get things working if i'm using only one db at a time, this means there is no issue with data source configuration.
Can you try this last line once (in same order) :-
targetDataSource.put(DataSourceEnum.HSQL, dataSource1());
targetDataSource.put(DataSourceEnum.BACKUP, dataSource2());
routingDataSource.setTargetDataSources(targetDataSource);
routingDataSource.afterPropertiesSet();
I got the same issue and found a solution using the SchemaExport class of hibernate.
For each DataSourceEnum you can manually initialize the datasource.
here is my detailed answer to my own issue discription
I am using scheduled task to update my database like this:
public interface UserRatingManager {
public void updateAllUsers();
}
#Service
public class DefaultUserRatingManager implements UserRatingManager {
#Autowired
UserRatingDAO userRatingDAO;
#Override
#Transactional("txName")
public void updateAllUsers() {
List<String> userIds = userRatingDAO.getAllUserIds();
for (String userId : userIds) {
updateUserRating(userId);
}
}
}
public interface UserRatingDAO extends GenericDAO<UserRating, String> {
public void deleteAll();
public List<String> getAllUserIds();
}
#Repository
public class HibernateUserRatingDAO extends BaseDAO<UserRating, String> implements UserRatingDAO {
#Override
public List<String> getAllUserIds() {
List<String> result = new ArrayList<String>();
Query q1 = getSession().createQuery("Select userId from UserRating");
}
}
I configured the persistence like this:
#Configuration
#ComponentScan({ "com.estartup" })
#PropertySource("classpath:jdbc.properties")
#EnableTransactionManagement
#EnableScheduling
public class PersistenceConfig {
#Autowired
Environment env;
#Scheduled(fixedRate = 5000)
public void run() {
userRatingManager().updateAllUsers();
}
#Bean
public DataSource dataSource() {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource(env.getProperty("connection.url"), env.getProperty("connection.username"), env.getProperty("connection.password"));
driverManagerDataSource.setDriverClassName("com.mysql.jdbc.Driver");
return driverManagerDataSource;
}
public PersistenceConfig() {
super();
}
#Bean
public UserRatingUpdate userRatingUpdate() {
return new UserRatingUpdate();
}
#Bean
public UserRatingManager userRatingManager() {
return new DefaultUserRatingManager();
}
#Bean
public LocalSessionFactoryBean runnableSessionFactory() {
LocalSessionFactoryBean factoryBean = null;
try {
factoryBean = createBaseSessionFactory();
} catch (Exception e) {
e.printStackTrace();
}
return factoryBean;
}
private LocalSessionFactoryBean createBaseSessionFactory() throws IOException {
LocalSessionFactoryBean factoryBean;
factoryBean = new LocalSessionFactoryBean();
Properties pp = new Properties();
pp.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQLDialect");
pp.setProperty("hibernate.max_fetch_depth", "3");
pp.setProperty("hibernate.show_sql", "false");
factoryBean.setDataSource(dataSource());
factoryBean.setPackagesToScan(new String[] { "com.estartup.*" });
factoryBean.setHibernateProperties(pp);
factoryBean.afterPropertiesSet();
return factoryBean;
}
#Bean(name = "txName")
public HibernateTransactionManager runnableTransactionManager() {
HibernateTransactionManager htm = new HibernateTransactionManager(runnableSessionFactory().getObject());
return htm;
}
}
However, when I get to:
Query q1 = getSession().createQuery("Select userId from UserRating");
in the above HibernateUserRatingDAO I get an exception:
org.hibernate.HibernateException: createQuery is not valid without active transaction
at org.hibernate.context.internal.ThreadLocalSessionContext$TransactionProtectionWrapper.invoke(ThreadLocalSessionContext.java:352)
at com.sun.proxy.$Proxy63.createQuery(Unknown Source)
at com.estartup.dao.impl.HibernateUserRatingDAO.getAllUserIds(HibernateUserRatingDAO.java:36)
How can I configure to include my scheduled tasks in transactions ?
EDITED:
Here is the code for BaseDAO
#Repository
public class BaseDAO<T, ID extends Serializable> extends GenericDAOImpl<T, ID> {
private static final Logger logger = LoggerFactory.getLogger(BaseDAO.class);
#Autowired
#Override
public void setSessionFactory(SessionFactory sessionFactory) {
super.setSessionFactory(sessionFactory);
}
public void setTopAndForUpdate(int top, Query query){
query.setLockOptions(LockOptions.UPGRADE);
query.setFirstResult(0);
query.setMaxResults(top);
}
EDITED
Enabling Spring transaction prints the following log:
DEBUG [pool-1-thread-1] org.springframework.transaction.annotation.AnnotationTransactionAttributeSource - Adding transactional method 'updateAllUsers' with attribute: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; 'txName'
What is happening in this case is that since you are using userRatingManager() inside the configuration (where the actual scheduled method exists), the proxy that Spring creates to handle the transaction management for UserRatingUpdate is not being used.
I propose you do the following:
public interface WhateverService {
void executeScheduled();
}
#Service
public class WhateverServiceImpl {
private final UserRatingManager userRatingManager;
#Autowired
public WhateverServiceImpl(UserRatingManager userRatingManager) {
this.userRatingManager = userRatingManager;
}
#Scheduled(fixedRate = 5000)
public void executeScheduled() {
userRatingManager.updateAllUsers()
}
}
Also change your transaction manager configuration code to:
#Bean(name = "txName")
#Autowired
public HibernateTransactionManager runnableTransactionManager(SessionFactory sessionFactory) {
HibernateTransactionManager htm = new HibernateTransactionManager();
htm.setSessionFactory(sessionFactory);
return htm;
}
and remove factoryBean.afterPropertiesSet(); from createBaseSessionFactory
As I already mentioned, I used your code and created a small sample that works for me. Judging by the classes used, I assumed you are using Hibernate Generic DAO Framework. It's a standalone sample, the main() class is Main. Running it you can see the transactional related DEBUG messages in logs that show when a transaction is initiated and committed. You can compare my settings, jars versions used with what you have and see if anything stands out.
Also, as I already suggested you might want to look in the logs to see if proper transactional behavior is being used and compare that with the logs my sample creates.
I tried to replicate your problem so I integrated it in my Hibernate examples on GitHub:
You can run my CompanySchedulerTest and see it's working so this is what I did to run it:
I made sure the application context is aware of our scheduler
<task:annotation-driven/>
The scheduler is defined in its own bean:
#Service
public class CompanyScheduler implements DisposableBean {
private static final Logger LOG = LoggerFactory.getLogger(CompanyScheduler.class);
#Autowired
private CompanyManager companyManager;
private volatile boolean enabled = true;
#Override
public void destroy() throws Exception {
enabled = false;
}
#Scheduled(fixedRate = 100)
public void run() {
if (enabled) {
LOG.info("Run scheduler");
companyManager.updateAllUsers();
}
}
}
My JPA/Hibernate configs are in applicationContext-test.xml and they are configured for JPA according to the Spring framework indications, so you might want to double check your Hibernate settings as well.