I have two transaction managers configured in annotation-based configuration class:
#Configuration
#EnableTransactionManagement
public class DBConfig implements TransactionManagementConfigurer {
//...
#Override
public PlatformTransactionManager annotationDrivenTransactionManager() {
return defTransactionManager();
}
#Bean
#Qualifier("defSessionFactory")
public LocalSessionFactoryBean defSessionFactory() {
LocalSessionFactoryBean sfb = new LocalSessionFactoryBean();
sfb.setDataSource(defDataSource());
Properties props = new Properties();
//...
sfb.setHibernateProperties(props);
sfb.setPackagesToScan("my.package");
return sfb;
}
#Bean
#Qualifier("defTransactionManager")
public PlatformTransactionManager defTransactionManager() {
return new HibernateTransactionManager(defSessionFactory().getObject());
}
#Bean
#Qualifier("secondSessionFactory")
public LocalSessionFactoryBean secondSessionFactory() {
LocalSessionFactoryBean sfb = new LocalSessionFactoryBean();
sfb.setDataSource(secondDataSource());
Properties props = new Properties();
//...
sfb.setHibernateProperties(props);
sfb.setPackagesToScan("my.package.subpackage");
return sfb;
}
#Bean
#Qualifier("secondTM")
public PlatformTransactionManager secondTransactionManager() {
return new HibernateTransactionManager(secondSessionFactory().getObject());
}
}
My intention is use annotation transactions with two transaction managers.
Methonds annotated like this
#Transactional
public void method() {}
should be handled by defTransactionManager
And methods annotated like this
#Transactional("secondTM")
public void anotherMethod() {}
by secondTransactionManager
defTransactionManager works fine but when it comes to anotherMethod() I get:
org.hibernate.HibernateException: No Session found for current thread
When I use programmatic transaction management for anotherMethod (autowire secondSessionFactory, use TransactionTemplate) everything works fine.
In case of #EnableTranscationManagement Spring will use by-type lookup, you can provide your own lookup method to a single transaction manager, but it will not work for two tx managers
If you want to check how Spring determines the transaction to execute, you can try to debug the TransactionAspectSupport class. The key methods are setTransactionManagerBeanName and determineTransactionManager.
Just in case anyone runs into this problem, I found a solution:
#Configuration
#EnableTransactionManagement
#DependsOn("myTxManager")
#ImportResource("classpath:applicationContext.xml")
public class AppConfig implements TransactionManagementConfigurer {
#Autowired
private PlatformTransactionManager myTxManager;
...
#Override
public PlatformTransactionManager annotationDrivenTransactionManager() {
return this.myTxManager;
}
In this way, you can use a specific txManager defined in an xml configuration.
In case you want to define the txManager used on service-level, you shall remove the #EnableTransactionManagement annotation from the #Configuration class and specify the txManager in the #Transactional annotations, e.g.
#Service
#Transactional(value="myTxManager", readOnly = true)
public class MyServiceImpl implements MyService { ... }
Related
I am trying to migrate some data between a Postgres database and MongoDB using Spring Batch. I have a very simple ItemReader, ItemProcessor, and ItemWriter configured, and it everything works as intended. However, if I switch to a RepositoryItemReader, I'm getting the following error:
java.lang.IllegalStateException: Already value [org.springframework.jdbc.datasource.ConnectionHolder#684430c1] for key [HikariDataSource (HikariPool-1)] bound to thread
If I understand correctly, there is something wrong with the EntityManager or TransactionManager, but I cannot figure out what, and why it's working with a simple ItemReader that doesn't work with a repository, but it uses the same data source.
I would be very grateful for any help.
Here is my source db configuration:
package com.example.batch.primary;
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = "primaryEntityManagerFactory",
transactionManagerRef = "primaryTransactionManager",
basePackages = {"com.example.batch.primary"}
)
public class PrimaryDBConfig {
#Bean(name = "primaryDataSource")
#Primary
public DataSource primaryDatasource(){
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create()
.driverClassName("org.postgresql.Driver")
.url("jdbc:postgresql://localhost:5432/postgres")
.username("test")
.password("test");
return dataSourceBuilder.build();
}
#Bean(name = "primaryEntityManagerFactory")
#Primary
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(EntityManagerFactoryBuilder builder,
#Qualifier("primaryDataSource")
DataSource primaryDataSource){
return builder.dataSource(primaryDataSource)
.packages("com.example.batch.primary")
.build();
}
#Bean(name = "primaryTransactionManager")
public PlatformTransactionManager primaryTransactionManager(
#Qualifier("primaryEntityManagerFactory") EntityManagerFactory primaryEntityManagerFactory)
{
return new JpaTransactionManager(primaryEntityManagerFactory);
}
}
Here is the configuration of MongoDB:
package com.example.batch.secondary;
#EnableMongoRepositories(basePackages = "com.example.batch.secondary")
#Configuration
public class MongoDBConfig {
#Bean
public MongoClient mongo() {
ConnectionString connectionString = new ConnectionString("mongodb+srv://mongoadmin:blablabla.mongodb.net/?retryWrites=true&w=majority");
MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
.applyConnectionString(connectionString)
.build();
return MongoClients.create(mongoClientSettings);
}
#Bean
public MongoTemplate mongoTemplate() throws Exception {
return new MongoTemplate(mongo(), "test");
}
}
Here is the RepositoryItemReader:
package com.example.batch.stepcomponents;
#Component
public class RepositoryReader extends RepositoryItemReader<Partner> {
public RepositoryReader(#Autowired PartnerRepository partnerRepository){
setRepository(partnerRepository);
setPageSize(1);
setSort(Map.of("id", Sort.Direction.ASC));
setMethodName("findAll");
}
}
Batch Config:
#Configuration
#EnableBatchProcessing
public class BatchConfig {
#Autowired
public JobBuilderFactory jobBuilderFactory;
#Autowired
public StepBuilderFactory stepBuilderFactory;
#Autowired
RepositoryReader repositoryReader;
#Autowired
CustomWriter customWriter;
#Autowired
CustomProcessor customProcessor;
#Bean
public Job createJob() {
return jobBuilderFactory.get("MyJob")
.incrementer(new RunIdIncrementer())
.flow(createStep())
.end()
.build();
}
#Bean
public Step createStep() {
return stepBuilderFactory.get("MyStep")
.<Partner, Student> chunk(1)
.reader(repositoryReader)
.processor(customProcessor)
.writer(customWriter)
.build();
}
}
So I tried taking out the EntityManagerFactory and the TransactionManager, and now it works. I guess they are already initialized automatically when starting up the server..
Yes, by default, if you provide a DataSource bean, Spring Batch will use a DataSourceTransactionManager, not the JPA one as you expect. This is explained in the Javadoc of EnableBatchProcessing:
The transaction manager provided by this annotation will be of type:
* ResourcelessTransactionManager if no DataSource is provided within the context
* DataSourceTransactionManager if a DataSource is provided within the context
In order to use the JPA transaction manager, you need to configure a custom a BatchConfigurer and override getTransactionManager, something like:
#Bean
public BatchConfigurer batchConfigurer(DataSource dataSource, EntityManagerFactory entityManagerFactory) {
return new DefaultBatchConfigurer(dataSource) {
#Override
public PlatformTransactionManager getTransactionManager() {
return new JpaTransactionManager(entityManagerFactory);
}
};
}
Note this will not be required anymore starting from v5, see:
Revisit the configuration of infrastructure beans with #EnableBatchProcessing
Spring Batch 5.0.0-M6 and 4.3.7 are out!
You can also set the JPA transaction manager on your step:
#Bean
public Step createStep(JpaTransactionManager jpaTransactionManager) {
return stepBuilderFactory.get("MyStep")
.<Partner, Student> chunk(1)
.reader(repositoryReader)
.processor(customProcessor)
.writer(customWriter)
.transactionManager(jpaTransactionManager)
.build();
}
Adding 'spring-data-jpa' as a dependency will automatically configure aJpaTransactionManager if no other TransactionManager is defined
I want to test only the persistence layer using #DataJpaTest, and I have two datasource configurations, one in src/main and the other is src/test, and I am using #primary on test datasource to get picked up only, but main datasource get picked up also.
src/main configuration
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(basePackages = "com.****.repository",
entityManagerFactoryRef = "platformEntityManagerFactory",
transactionManagerRef = "platformTransactionManager"
)
#Import(CommonPersistenceConfig.class)
public class PlatformPersistenceConfig {
#Value("classpath:application.yml")
private Resource resource;
#Bean
#Qualifier("platformTransactionManager")
public PlatformTransactionManager platformTransactionManager(#Qualifier("platformEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
// code...
}
#Bean
public LocalContainerEntityManagerFactoryBean platformEntityManagerFactory(DataSource dataSource,
HibernateProperties hibernateProperties) {
// code...
}
#Bean
public DataSource getDataSource() throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
// code...
}
}
src/test configuration
#Configuration
#Import({CommonPersistenceConfig.class, DbPropertyConfig.class})
public class DbTestSetupConfig {
#Autowired
private TestDatasourceProperties dbProperties;
#Primary
#Bean(destroyMethod = "close")
public DataSource getDataSource() throws Exception {
// code...
}
#PostConstruct
public void dbSetup() throws Exception {
// code...
}
}
my test
#DataJpaTest
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class StripeCardRepositoryTest {
#Autowired
StripeCardRepository stripeCardRepository;
#Test
public void test() {
}
}
The point of #AutoConfigureTestDatabase is to replace an existing datasource defined in src/main/.. with a datasource defined in src/test. But to do so you must inform it what strategy it must use. You have chosen AutoConfigureTestDatabase.Replace.NONE therefore it does not replace the main datasource with the one that you need.
Try to switch to AutoConfigureTestDatabase.Replace.ANY and it will correctly replace it with the one defined in test.
Also probably you show this in the documentation
In the case of multiple DataSource beans, only the #Primary
DataSource is considered.
This applies in the case that you have multiple datasources in project defined. It means that it would replace only the primary datasource with the strategy defined. It does not mean that it would pick only the primary and ignore the others.
I'm writing a SpringBoot REST server. I have some problems with the data access layer. In my saveReport() service method, if I call save() method of JPA repository, I can't see any records in the db. If I use saveAndFlush() method of JPA repository, I get the following exception:
Caused by: javax.persistence.TransactionRequiredException: no transaction is in progress
Db config class:
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = "qaEntityManagerFactory",
basePackages = { "qa.repository" }
)
public class QADbConfig {
#Bean(name = "qaDataSource")
#ConfigurationProperties(prefix = "spring.qa-datasource")
public HikariDataSource dataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
#Bean(name = "qaEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean
entityManagerFactory(
EntityManagerFactoryBuilder builder,
#Qualifier("qaDataSource") DataSource dataSource
) {
return builder
.dataSource(dataSource)
.packages("qa.model")
.persistenceUnit("qa")
.build();
}
#Bean(name = "qaTransactionManager")
public PlatformTransactionManager transactionManager(
#Qualifier("qaEntityManagerFactory") EntityManagerFactory
entityManagerFactory
) {
return new JpaTransactionManager(entityManagerFactory);
}
}
In my service layer, I added #Transactional
#Service
public class ReportService implements IReportService {
static final ObjectMapper objectMapper = new ObjectMapper();
#Autowired
ReportRepository reportJpaRepository;
#Transactional(propagation=Propagation.REQUIRED)
#Override
public Report saveReport(Report report) {
reportJpaRepository.save(report); // I get no transaction exception if I use saveAndFlush method here
return report;
}
Also in the repository class, I added #Transactional
#Transactional
#Repository
public interface ReportJpaRepository extends JpaRepository<Report, Integer> {
List<Report> findByPrivateReport(Boolean privateReport);
}
Do you see what is wrong with my code?
Thanks
I have RepositoryConfig extending Neo4jConfiguration. The latter sets up a number of beans with #Bean annotated methods. RepositoryConfigoverrides getGraphDatabaseService which is invoked before any fields in RepositoryConfig are autowired. That is a problem since I want to use the autowired stuff inside the getGraphDatabaseServicemethod.
#ConfigurationProperties(prefix = "neo4j")
public class RepositoryProperties {
[...]
}
#Configuration
#EnableNeo4jRepositories("com.foo.bar")
#EnableConfigurationProperties(RepositoryProperties.class)
public class RepositoryConfig extends Neo4jConfiguration {
#Autowired
private RepositoryProperties properties;
#Override
#Bean(name = "graphDatabaseService", destroyMethod = "shutdown")
public GraphDatabaseService getGraphDatabaseService() {
[...] // properties is 'null' at this point
}
#PostContstruct
public void foo() {
[...] // properties is initiated OK here
}
}
Why is getGraphDatabaseServicebeing called before autowiring is complete? I guess it has to do with the inheritance... If I remove the inheritance then autowiring is complete at the time getGraphDatabaseServiceis called. I've also tried annotating the method with #DependsOn, with no luck.
Any ideas is much appreciated!
Yes, I have seen this too occasionally. I think there are two workarounds.
Option 1. Autowire the bean definition
#Override
#Bean(name = "graphDatabaseService", destroyMethod = "shutdown")
#Autowired
public GraphDatabaseService getGraphDatabaseService() {
[...] // properties is 'null' at this point
}
Option 2. Inject the bean
#Override
#Bean(name = "graphDatabaseService", destroyMethod = "shutdown")
public GraphDatabaseService getGraphDatabaseService(#Autowired RepositoryProperties properties) {
// can probably delete the Config member with this approach
[...] // properties is 'null' at this point
}
I am trying to create a Spring MVC application leveraging JPA for its persistence layer. Unfortunately, I was getting a NullPointerException when accessing the EntityManager as Spring does not appear to be injecting it. My configuration is all annotation-based with #EnableWebMvc. After some searching, I added #Transactional on my DAO and #EnableTransactionManagement on my #Configuration class. Then I got an error about not having a DataSource. Supposedly, a class with #EnableTransactionManagement needs to implement TransactionManagementConfigurer. However, I am having problems figuring out how to create the DataSource as well as why it cannot get it from my persistence.xml.
I would greatly appreciate any help in trying to get the EntityManager injected into my DAO.
My #Configuration class
#Configuration
#EnableWebMvc
#EnableTransactionManagement
#ComponentScan("com.example.myapp")
public class MvcConfig extends WebMvcConfigurerAdapter
implements TransactionManagementConfigurer {
private static final boolean CACHE_ENABLED = true;
private static final String TEMPLATE_PATH = "/WEB-INF/freemarker";
private static final String TEMPLATE_SUFFIX = ".ftl";
private static final Logger LOG = Logger.getLogger( MvcConfig.class );
#Override
public void addResourceHandlers( ResourceHandlerRegistry registry ) {
registry.addResourceHandler( "/stylesheets/**" ).addResourceLocations( "/stylesheets/" );
}
#Bean
public FreeMarkerConfigurer configureFreeMarker() {
final FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath( TEMPLATE_PATH );
return configurer;
}
#Bean
public ViewResolver configureViewResolver() {
final FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
resolver.setCache( CACHE_ENABLED );
resolver.setSuffix( TEMPLATE_SUFFIX );
return resolver;
}
#Bean
#Override
public PlatformTransactionManager annotationDrivenTransactionManager() {
return new DataSourceTransactionManager();
}
}
My DAO
#Component
#Transactional
public class MyDAO {
private static final Logger LOG = Logger.getLogger( MyDAO.class );
#PersistenceContext
private EntityManager entityManager;
public MyClass getMyClass() {
LOG.debug( "getMyClass()" );
final CriteriaQuery<MyClass> query = criteriaBuilder.createQuery( MyClass.class );
// more code here, but it breaks by this point
return myData;
}
}
My Updated Code
I have reached the point in which it almost all works. The EntityManager is being injected properly. However, transactions are not working. I get errors if I try to use a RESOURCE_LOCAL approach so I am looking at JTA managed transactions. When I add #Transactional on any of my DAO methods, I get a "Transaction marked for rollback" error with no further details in any log files to assist troubleshooting. If I remove the annotation from a basic read-only select, the select will work perfectly fine (not sure if I should even be putting the annotation on select-only methods). However, I obviously need this working on methods which perform db writes. If I debug through the code, it seems to retrieve the data perfectly fine. However, as it returns from the method, the javax.transaction.RollbackException gets thrown. From my understanding of everything, it seems as if the exception occurs in the AOP post-method processing.
My #Configuration class
#Configuration
#EnableWebMvc
#EnableTransactionManagement
#ComponentScan("com.example.myapp")
public class MvcConfig extends WebMvcConfigurerAdapter {
private static final boolean CACHE_ENABLED = true;
private static final String TEMPLATE_PATH = "/WEB-INF/freemarker";
private static final String TEMPLATE_SUFFIX = ".ftl";
private static final Logger LOG = Logger.getLogger( MvcConfig.class );
#Override
public void addResourceHandlers( ResourceHandlerRegistry registry ) {
registry.addResourceHandler( "/stylesheets/**" ).addResourceLocations( "/stylesheets/" );
}
#Bean
public FreeMarkerConfigurer configureFreeMarker() {
final FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath( TEMPLATE_PATH );
return configurer;
}
#Bean
public ViewResolver configureViewResolver() {
final FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
resolver.setCache( CACHE_ENABLED );
resolver.setSuffix( TEMPLATE_SUFFIX );
return resolver;
}
#Bean
public PlatformTransactionManager transactionManager() {
return new JtaTransactionManager();
}
#Bean
public AbstractEntityManagerFactoryBean entityManagerFactoryBean() {
LocalEntityManagerFactoryBean factory = new LocalEntityManagerFactoryBean();
factory.setPersistenceUnitName( "my_db" );
return factory;
}
}
In my application I didn't implement TransactionManagerConfigurer interface. I use next code to configure JPA (with Hibernate implementation). You can do the same in your configuration class.
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(dataSource());
factoryBean.setPackagesToScan(new String[] {"com.dimasco.springjpa.domain"});
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setShowSql(true);
//vendorAdapter.setGenerateDdl(generateDdl)
factoryBean.setJpaVendorAdapter(vendorAdapter);
Properties additionalProperties = new Properties();
additionalProperties.put("hibernate.hbm2ddl.auto", "update");
factoryBean.setJpaProperties(additionalProperties);
return factoryBean;
}
#Bean
public DataSource dataSource() {
final ComboPooledDataSource dataSource = new ComboPooledDataSource();
try {
dataSource.setDriverClass(driverClass);
} catch (PropertyVetoException e) {
throw new RuntimeException(e);
}
dataSource.setJdbcUrl(jdbcUrl);
dataSource.setUser(user);
dataSource.setPassword(password);
dataSource.setMinPoolSize(3);
dataSource.setMaxPoolSize(15);
dataSource.setDebugUnreturnedConnectionStackTraces(true);
return dataSource;
}
#Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactoryBean().getObject());
return transactionManager;
}
#Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation(){
return new PersistenceExceptionTranslationPostProcessor();
}
Hope this will help you)
EDIT:
You can get datasource using JNDI lookup:
#Bean
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
More details you can find in this article. There is example with JndiDatasourceConfig class.
EDIT 2:
I ahve persistence.xml in my project, but it is empty:
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="JPA_And_Spring_Test">
</persistence-unit>
</persistence>
And I didn't specify any persistent unit name in my java configuration.
The following might help, even though it uses XML-based configuration:
https://github.com/springinpractice/sip13/blob/master/helpdesk/src/main/resources/spring/beans-repo.xml
It uses Spring Data JPA, but you don't have to do that. Use
#PersistenceContext private EntityManager entityManager;
(But consider Spring Data JPA as it provides very capable DAOs out of the box.)
Side note: For DAOs, favor #Repository over #Component. Both work for component scanning, but #Repository better describes the intended use.