NoUniqueBeanDefinitionException when setting up multiple JPA datasources - spring-boot

I've followed many tutorials and reviewed many answers (in SO and other sites), and I'm quite confident I didn't miss anything, but I can't get two datasources/EntityManagers to work in my environment.
This is what I have
DbADataSourceConfig.java
package com.myorg.rest.config.dba;
import ...
#Configuration
// #EnableJpaRepositories(
// basePackages = "com.myorg.rest.dao.dbA",
// entityManagerFactoryRef = "dbAEntityManager",
// transactionManagerRef = "dbATransactionManager"
// )
#EnableTransactionManagement
public class DbADataSourceConfig {
#Autowired
private EnvProperties settings;
#Bean
#Primary
public DataSource prjDataSource() {
DataSourceProperties ds = settings.getdbADatasource();
return ds.initializeDataSourceBuilder().type(BasicDataSource.class).build();
}
#Bean(name = "dbAEntityManager")
#Primary
public LocalContainerEntityManagerFactoryBean dbAEntityManager() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(prjDataSource());
em.setPackagesToScan(new String[] { "com.myorg.model.entities.dba" });
em.setPersistenceUnitName("dbAUnit");
JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
em.setJpaProperties(additionalProperties());
// em.afterPropertiesSet();
return em;
}
#Bean(name = "dbATransactionManager")
#Primary
public PlatformTransactionManager dbATransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(dbAEntityManager().getObject());
return transactionManager;
}
Properties additionalProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect");
return properties;
}
}
DbBDataSourceConfig.java
package com.myorg.rest.config.dbb;
import ...
#Configuration
// #EnableJpaRepositories(
// basePackages = "com.myorg.rest.dao.dbB",
// entityManagerFactoryRef = "dbBEntityManager",
// transactionManagerRef = "dbBTransactionManager"
// )
#EnableTransactionManagement
public class DbBDataSourceConfig {
#Autowired
private EnvProperties settings;
#Bean
#Primary
public DataSource prjDataSource() {
DataSourceProperties ds = settings.getdbBDatasource();
return ds.initializeDataSourceBuilder().type(BasicDataSource.class).build();
}
#Bean(name = "dbBEntityManager")
#Primary
public LocalContainerEntityManagerFactoryBean dbBEntityManager() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(prjDataSource());
em.setPackagesToScan(new String[] { "com.myorg.model.entities.dbb" });
em.setPersistenceUnitName("dbBUnit");
JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
em.setJpaProperties(additionalProperties());
// em.afterPropertiesSet();
return em;
}
#Bean(name = "dbBTransactionManager")
#Primary
public PlatformTransactionManager dbBTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(dbBEntityManager().getObject());
return transactionManager;
}
Properties additionalProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect");
return properties;
}
}
AbstractDBADAO.java
package com.myorg.rest.dao.dba.base;
public abstract class AbstractDBADAO<T extends Serializable> implements IAbstractJpaDAO<T> {
private Class<T> clazz;
#PersistenceContext(unitName = "dbAUnit")
private EntityManager entityManager;
public AbstractDBADAO(Class<T> clazzToSet) {
this.clazz = clazzToSet;
}
public T findOne(String id) {
return entityManager.find(clazz, id);
}
public List<T> findAll() {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<T> entityQuery = criteriaBuilder.createQuery(clazz);
entityQuery.from(clazz);
return entityManager.createQuery(entityQuery).getResultList();
}
public void create(T entity) {
entityManager.persist(entity);
}
public T update(T entity) {
return entityManager.merge(entity);
}
public void delete(T entity) {
entityManager.remove(entity);
}
public void deleteById(String entityId) {
T entity = findOne(entityId);
delete(entity);
}
}
and last IAbstractJpaDAO.java
package com.myorg.rest.dao.dba.base;
import ...
public interface IAbstractJpaDAO<T extends Serializable> {
T findOne(String id);
List<T> findAll();
void create(T entity);
T update(T entity);
void delete(T entity);
void deleteById(String entityId);
}
Of course, there's also a bunch of #Repositorys extending IAbstractDAO interface and a corresponding bunch of #Services extending AbstractDAO<T>, but those are just skeletons like
EntityARepo.java
package com.myorg.rest.dao.dba.repositories;
import ...
#Repository
public interface EntityARepo extends IAbstractJpaDAO<EntityA> {
}
EntityAService.java
package com.myorg.rest.dao.dba.services;
import ...
#Service
public class EntityAService extends AbstractDBADAO<EntityA> implements EntityARepo {
public EntityAService() {
super(EntityA.class);
}
}
Similarly, there's a whole bunch of clases set up for the second database (dbB).
The problem
No matter what I do, I get the following problem (or something very similar):
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'javax.persistence.EntityManager' available: expected single matching bean but found 2: org.springframework.orm.jpa.SharedEntityManagerCreator#0,org.springframework.orm.jpa.SharedEntityManagerCreator#1
at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:220)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1285)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1227)
From what I understand, the #PersistenceContext can not resolve to the correct LocalContainerEntityManagerFactoryBean. They both have setPersistenceUnitName('dbXUnit'), so they should be able to get resolved correctly, right?.
I do not have any persistence.xml file, I'm doing everything programmatically.
What I've tried so far
Obviously, the code above.
Also, you will notice there's a commented section for #EnableJpaRepositories; that doesn't work either.
I've also tried to build a PersistenceUnitManager for each of the DataSource config classes to no success.
I've tried #Autowired, with and without the #PersistenceContext, no luck either.
I've tried #PersistenceContext on a setter and using #Qualifier on the input EntityManager, no luck either.
I've tried with and without the #Primary decorators.
Please keep in mind any inconsistencies between the files above posted are surely a copy/paste error, I've had to edit names in order to keep confidentiality. Also note that the code I have compiles, runs, and doesn't output any other errors except for the one listed above. Also, with only one of those Database Config classes set up, the project runs as expected, and I can execute the tests on that connection.
The question
Can anyone spot why the #PersistenceContext is not wiring the
correct dependency and complains of a duplicate? or
Should I do things in a completely different manner?

You are going the correct way so far.
You need 2 of each:
EntityManager/factory
DataSource
TransactionManager
JPARepository-Configs
The key to success is IMO the correct and unique naming of them all (2x4=8 different names). #Primary and #Secondary do not help here.

Related

Is there a way to stop Spring from rollbacking every transaction in a test method and do a Rollback after it executes?

I'm trying to write some integration tests for an app that I write as a practise. I have a test class with #Transactional annotation on it, to stop tests from committing any changes to db. I've noticed that rollback happens for every method of JPA repository I use. When I save an entity and then try to find a values for mentioned entity I get an empty List since that entity does not exists in DB. So do I have to change my approach or is there a way to wrap all of test method contents into a single transaction that will be rolledback after test method is done running?
Service that is tested:
#Service
public class FridgeServiceImpl implements FridgeService {
private final FridgeRepository fridgeRepository;
public FridgeServiceImpl(FridgeRepository fridgeRepository) {
this.fridgeRepository = fridgeRepository;
}
#Override
public Fridge save(Fridge fridge) {
return fridgeRepository.save(fridge);
}
#Override
public List<IngredientInFridge> getAllInFridge(Long id) {
return fridgeRepository.findAllInFridge(id);
}
#Override
public List<IngredientInFridge> getAllToExpireAfterDate(LocalDate date) {
return fridgeRepository.findAllWithExpirationDateAfter(date);
}
Repository:
#Repository
#Transactional
public interface FridgeRepository extends JpaRepository<Fridge, Long> {
#Query(value = "select i from Fridge f join f.fridgeContents i where i.id.fridgeId = :fridgeId")
List<IngredientInFridge> findAllInFridge(#Param("fridgeId") Long fridgeId);
#Query(value = "select i from IngredientInFridge i inner join IngredientGrammage ig on i.ingredient.id = ig.ingredient.id where i.fridge.id = :fridgeId " +
"and ig.recipe.id = :recipeId")
Set<IngredientInFridge> getIngredientsAvailableForRecipe(#Param("recipeId") Long recipeId, #Param("fridgeId") Long fridgeId);
#Query(value = "select i from IngredientInFridge i where i.expirationDate <= :date")
List<IngredientInFridge> findAllWithExpirationDateAfter(#Param("date") LocalDate date);
Test class:
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = {TestConfig.class})
#Transactional
class FridgeServiceImplTest {
#Autowired
FridgeService fridgeService;
Ingredient ingredient;
Fridge fridge;
IngredientInFridge ingredientInFridge;
#BeforeEach
public void setUp() {
ingredient = new Ingredient(100, "Chicken", UnitOfMeasure.G);
fridge = new Fridge(new HashSet<>());
ingredientInFridge = new IngredientInFridge(ingredient, fridge, 50, LocalDate.of(2020, 12, 30));
fridge.addToFridge(ingredientInFridge);
}
#Test
void getAllInFridge_ThenReturn_1() {
//given
var savedFridge = fridgeService.save(fridge); //save an entity
//when
var ingredientsInFridge = fridgeService.getAllInFridge(savedFridge.getId()); //empty List since fridgeService.save(fridge) was rolledback.
//then
Assertions.assertEquals(1, savedFridge.getFridgeContents().size());
Assertions.assertEquals(1, ingredientsInFridge.size()); // failed assertion
}
Test config:
#Configuration
#EnableJpaRepositories(value = "com.calculate.calories.repository")
#PropertySource("classpath:applicationTest.properties")
#ComponentScan(basePackages = {"com.calculate.calories.service", "com.calculate.calories.repository"})
#EntityScan("com.calculate.calories.model")
public class TestConfig {
#Autowired
private Environment environment;
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean em
= new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource());
em.setPackagesToScan("com.calculate.calories.model");
JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
em.setJpaProperties(additionalProperties());
return em;
}
#Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(Objects.requireNonNull(environment.getProperty("spring.datasource.driver-class-name")));
dataSource.setUrl(Objects.requireNonNull(environment.getProperty("spring.datasource.url")));
dataSource.setUsername(Objects.requireNonNull(environment.getProperty("spring.datasource.username")));
dataSource.setPassword(Objects.requireNonNull(environment.getProperty("spring.datasource.password")));
return dataSource;
}
#Bean
public PlatformTransactionManager transactionManager() {
//JpaTransactionManager transactionManager = new JpaTransactionManager();
//transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
return new DataSourceTransactionManager(dataSource());
}
#Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
return new PersistenceExceptionTranslationPostProcessor();
}
Properties additionalProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.hbm2ddl.auto", "none");
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect");
return properties;
}
I tried to use JpaTransactionManager instead of DataSourceTransactionManager but if I do so, there are no rollbacks for the tests and db is flooded with duplicate data. I've also attempted changing
#Transactional from class level to method level but it changed nothing.
I've seen this post: Does #Transactional annotation on test methods rollback every transaction in the annotated method before it gets to the end?
But when I try to use the EntityManager.flush() after saving it throws javax.persistence.TransactionRequiredException: no transaction is in progress

problem with writing to 2 databases from Spring boot

I was trying this code from github:https://github.com/kodinor/spring-data-many-dbs
It's an example of how to use multiple db's from a Spring-boot application. it worked fine but I added the Spring-boot-starter-web dependency and now I'm getting an error:
>Method requestMappingHandlerMapping in >org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$En>ableWebMvcConfiguration required a single bean, but 2 were found:
> - productDSEmFactory: defined by method 'productDSEmFactory' in class >path resource [com/kodinor/configuration/ProductDBConfiguration.class]
> - userDSEmFactory: defined by method 'userDSEmFactory' in class path >resource [com/kodinor/configuration/UserDBConfiguration.class]
>
>
>Action:
>
>Consider marking one of the beans as #Primary, updating the consumer to >accept multiple beans, or using #Qualifier to identify the bean that should >be consumed<br>
I have two config files:
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(basePackageClasses = UserRepository.class, entityManagerFactoryRef = "userDSEmFactory", transactionManagerRef = "userDSTransactionManager")
public class UserDBConfiguration {
#Primary
#Bean
#ConfigurationProperties("spring.datasource1")
public DataSourceProperties userDSProperties() {
return new DataSourceProperties();
}
#Primary
#Bean
public DataSource userDS(#Qualifier("userDSProperties") DataSourceProperties userDSProperties) {
return userDSProperties.initializeDataSourceBuilder().build();
}
#Bean
public LocalContainerEntityManagerFactoryBean userDSEmFactory(#Qualifier("userDS") DataSource userDS, EntityManagerFactoryBuilder builder) {
return builder.dataSource(userDS).packages(User.class).build();
}
#Primary
#Bean
public PlatformTransactionManager userDSTransactionManager(EntityManagerFactory userDSEmFactory) {
return new JpaTransactionManager(userDSEmFactory);
}
}
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(basePackageClasses = ProductRepository.class, entityManagerFactoryRef = "productDSEmFactory", transactionManagerRef = "productDSTransactionManager")
public class ProductDBConfiguration {
#Bean
#ConfigurationProperties("spring.datasource2")
public DataSourceProperties productDSProperties() {
return new DataSourceProperties();
}
#Bean
public DataSource productDS(#Qualifier("productDSProperties") DataSourceProperties productDSProperties) {
return productDSProperties.initializeDataSourceBuilder().build();
}
#Bean
public LocalContainerEntityManagerFactoryBean productDSEmFactory(#Qualifier("productDS") DataSource productDS, EntityManagerFactoryBuilder builder) {
return builder.dataSource(productDS).packages(Product.class).build();
}
#Bean
public PlatformTransactionManager productDSTransactionManager(EntityManagerFactory productDSEmFactory) {
return new JpaTransactionManager(productDSEmFactory);
}
}
Application.properties:
spring.jpa.hibernate.ddl-auto=create
spring.jpa.generate-ddl=true
spring.datasource1.url=jdbc:mysql://localhost:3306/userdb?autoReconnect=true&useSSL=false
spring.datasource1.username=user
spring.datasource1.password=pass
spring.datasource2.url=jdbc:mysql://localhost:3306/productdb?autoReconnect=true&useSSL=false
spring.datasource2.username=user
spring.datasource2.password=pass
And a simple main app that adds some init data:
#SpringBootApplication
public class SpringDataManyDbsApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDataManyDbsApplication.class, args);
}
#Autowired
private UserRepository userRepository;
#Autowired
private ProductRepository productRepository;
#PostConstruct
void init () {
User user = new User("john", "doe");
userRepository.save(user);
Product product = new Product("chair", 5);
productRepository.save(product);
}
}
I tried to add the #Primary annotation to userDSEmFactory. The error goes away but the product data isn't saved anymore. Any ideas how to save this problem? I don't have a lot of experience with Spring-boot and I've read dozens of articles but many seem to do things in a different way. Thanks so much for helping me out!
update
I've added the #Primary annotation like this:
#Primary
#Bean
public LocalContainerEntityManagerFactoryBean userDSEmFactory(#Qualifier("userDS") DataSource userDS, EntityManagerFactoryBuilder builder) {
return builder.dataSource(userDS).packages(User.class).build();
}
No there are no more errors, but only the user is being saved in the primary db and nothing is being saved to the product (second) database.
So if anybody has suggestions what could be the cause of that..
You are missing #Primary annotation on your userDSEmFactory
#Primary
#Bean
public LocalContainerEntityManagerFactoryBean userDSEmFactory(#Qualifier("userDS") DataSource userDS, EntityManagerFactoryBuilder builder) {
return builder.dataSource(userDS).packages(User.class).build();
}
And also it is goot to use once #EnableTransactionManagement at your main class.

Spring Boot giving warning Custom isolation level specified but no actual transaction initiated

I am getting warning Custom isolation level specified but no actual transaction initiated when I use #Transactional annotation with isolation and propagation.But my query executes fine.Why method call is not coming under Transaction boundary.
#SpringBootApplication(scanBasePackages= {"com.mytest.txntest"})
#EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class Application {
public static void main( String[] args )
{
SpringApplication.run(Application.class, args);
}
}
My Data Source Configuration looks like below.
#Configuration
#EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
#EnableJpaRepositories(basePackages = { "com.mytest.txntest" }, entityManagerFactoryRef = "testEntityManagerFactory", transactionManagerRef = "testTransactionManager")
#EnableTransactionManagement
public class TestDBConfig {
#Resource
private Environment env;
#Bean(name="testEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean testEntityManager(){
LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
entityManager.setDataSource(testDataSource());
Properties properties = new Properties();
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.DB2Dialect");
properties.setProperty("persistenceProviderClassName", "org.hibernate.ejb.HibernatePersistence");
entityManager.setJpaProperties(properties);
entityManager.setPersistenceUnitName("Test_DB");
entityManager.setPackagesToScan("com.mytest.txntest.entities");
entityManager.setJpaVendorAdapter(jpaVendorAdapter());
return entityManager;
}
#Bean(name="testDataSource")
public DataSource testDataSource() {
DataSource dataSource = DataSourceBuilder
.create()
.username(env.getProperty("test.username"))
.password(env.getProperty("test.password"))
.url(env.getProperty("test.url"))
.driverClassName(env.getProperty("test.datasource.driverClassName"))
.build();
return dataSource;
}
#Bean
public HibernateJpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateAdatper = new HibernateJpaVendorAdapter();
hibernateAdatper.setDatabasePlatform("org.hibernate.dialect.DB2Dialect");
hibernateAdatper.setShowSql(true);
return hibernateAdatper;
}
#Bean("testTransactionManager")
public JpaTransactionManager jpaTransactionManager(#Qualifier("testEntityManagerFactory") EntityManagerFactory entityMangerFactory) {
JpaTransactionManager jpsTxnManager = new JpaTransactionManager();
jpsTxnManager.setEntityManagerFactory(entityMangerFactory);
return new JpaTransactionManager();
}
}
My DAO Class looks like below.
public TestDAO {
#PersistenceContext
public void setEntityManager(EntityManager entityManager)
{
this.em = entityManager;
}
private EntityManagerFactory entityManagerFactory;
public void setEntityManagerFactory(EntityManagerFactory emf)
{
this.entityManagerFactory = emf;
em = entityManagerFactory.createEntityManager();
}
#PersistenceContext
protected EntityManager em;
#Transactional(propagation=Propagation.NOT_SUPPORTED,isolation=Isolation.READ_UNCOMMITTED,rollbackFor=Throwable.class)
public Webacct getUserNameInfo(String userName,String dob)
{
Query q = em.createNamedQuery("testQuery");
}
}
}
Am I missing any configuration. Using Spring Boot 1.5.9
You’re using “NOT_SUPPORTED” propagation level which essentially executes the code inside that method without a transaction. You should use any other propagation level such as REQUIRED or REQUIRES_NEW.

Spring Data with JPA not rolling back transaction on error

We have Spring Data configured for JPA. A Service Transaction method doesn't get rolled back for an error (e.g. a DB ConstraintViolationException).
The closest I could find was this
(Transaction not rolling back) Spring-data, JTA, JPA, Wildfly10
but we don't have any XML configuration, all of our configuration is Java-based.
Essentially, a service method looks like this: no errors are caught, everything thrown.
#Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class, readOnly = false)
public void insertEvent() throws Exception {
// Part 1
EventsT event = new EventsT();
// populate it..
eventsDAO.save(event);
// Part 2 - ERROR HAPPENS HERE (Constraint Violation Exception)
AnswersT answer = new AnswersT();
// populate it..
answersDAO.save(answer);
}
Part 2 fails. But after the error and return, I see that the Event (Part 1) is still populated in the DB.
We also tried various combinations of #Transactional, nothing worked:
#Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class, readOnly = false)
#Transactional(readOnly = false)
#Transactional(propagation = Propagation.REQUIRED, rollbackFor = ConstraintViolationException.class, readOnly = false)
Spring Data CRUD DAO Interfaces:
#Repository
public interface EventsDAO extends JpaRepository<EventsT, Integer> {
}
#Repository
public interface AnswersDAO extends JpaRepository<AnswersT, Integer> {
}
JpaConfig:
#Configuration
#EnableJpaRepositories(basePackages = "com.myapp.dao")
#PropertySource({ "file:${conf.dir}/myapp/db-connection.properties" })
public class JpaConfig {
#Value("${jdbc.datasource}")
private String dataSourceName;
#Bean
public Map<String, Object> jpaProperties() {
Map<String, Object> props = new HashMap<String, Object>();
props.put("hibernate.dialect", PostgreSQL95Dialect.class.getName());
//props.put("hibernate.cache.provider_class", HashtableCacheProvider.class.getName());
return props;
}
#Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(true);
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setDatabase(Database.POSTGRESQL);
return hibernateJpaVendorAdapter;
}
#Bean
public PlatformTransactionManager transactionManager() throws NamingException {
return new JpaTransactionManager( entityManagerFactory().getObject() );
}
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws NamingException {
LocalContainerEntityManagerFactoryBean lef = new LocalContainerEntityManagerFactoryBean();
lef.setDataSource(dataSource());
lef.setJpaPropertyMap(this.jpaProperties());
lef.setJpaVendorAdapter(this.jpaVendorAdapter());
lef.setPackagesToScan("com.myapp.domain", "com.myapp.dao");
return lef;
}
#Bean
public DataSource dataSource() throws NamingException {
return (DataSource) new JndiTemplate().lookup(dataSourceName);
}
}
Have there been any transaction rollback issues with Spring Data & JPA?
Believe it or not we fixed it. There were 2 parts to the solution:
1) Add #EnableTransactionManagement to JpaConfig as ledniov described, but that alone wasn't enough;
2) Also in JpaConfig in entityManagerFactory(), add the Service class package to the following setPackagesToScan. Previously, the domain object package was there, but the service object package was not. We added "myapp.service", the 2nd package.
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws NamingException {
LocalContainerEntityManagerFactoryBean lef = new LocalContainerEntityManagerFactoryBean();
lef.setDataSource(dataSource());
lef.setJpaPropertyMap(this.jpaProperties());
lef.setJpaVendorAdapter(this.jpaVendorAdapter());
lef.setPackagesToScan("myapp.domain", "myapp.service"); //NOTE: Service was missing
return lef;
}
You have to add #EnableTransactionManagement annotation to JpaConfig class in order to enable Spring's annotation-driven transaction management capability.

JPA with Spring MVC Configured via Annotations

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.

Resources