I am using spring-boot-starter-jdbc and trying to use multiple Jdbc DataSources, everything worked when I used named Beans for my JdbcTemplate and then used #Qualifier to inject the right JdbcTemplate into my repository.
Application:
package my.app;
#SpringBootApplication
#EnableAutoConfiguration
public class Application implements CommandLineRunner {
#Autowired
private MyRepository repository;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Override
public void run(String... args) throws Exception {
List<Stuff> stuff = repository.getStuff();
}
}
Configuration:
package my.app;
#Configuration
#ComponentScan
public class AppConfig {
#Bean(name = "datasource1")
#ConfigurationProperties(prefix = "db1.datasource")
public DataSource dataSource1() {
return DataSourceBuilder.create().build();
}
#Bean(name = "db1")
public DataSource db1(#Qualifier("datasource1" DataSource ds) {
return new JdbcTemplate(ds);
}
#Bean(name = "datasource2")
#ConfigurationProperties(prefix = "db2.datasource")
public DataSource dataSource2() {
return DataSourceBuilder.create().build();
}
#Bean(name = "db2")
public DataSource db1(#Qualifier("datasource1" DataSource ds) {
return new JdbcTemplate(ds);
}
}
Repository:
package my.app;
#Repository
public class MyRepository {
private JdbcTemplate db1;
private JdbcTemplate db2;
#Autowired
public class MyRepository(#Qualifier("db1") JdbcTemplate db1, #Qualifier("db2") JdbcTemplate db2) {
this.db1 = db1;
this.db2 = db2;
}
}
When I instantiate MyRepository everything is fine.
I did a little refactoring to try to make new classes for Db1JdbcTemplate and Db2JdbcTemplate so I could inject them without qualifiers. Unfortunately when I do this I get the following exception:
Here's what I attempted to do:
Removed named JdbcTemplate Beans from AppConfig:
package my.app;
#Configuration
#ComponentScan
public class AppConfig {
#Bean(name = "datasource1")
#ConfigurationProperties(prefix = "db1.datasource")
public DataSource dataSource1() {
return DataSourceBuilder.create().build();
}
#Bean(name = "datasource2")
#ConfigurationProperties(prefix = "db2.datasource")
public DataSource dataSource2() {
return DataSourceBuilder.create().build();
}
}
Created 2 new classes for named JdbcTemplates:
package my.app;
#Component
public class Db1JdbcTemplate extends JdbcTemplate {
#Autowired
public Db1JdbcTemplate(#Qualifier("datasource1") DataSource ds1) {
super(ds1);
}
}
package my.app;
#Component
public class Db2JdbcTemplate extends JdbcTemplate {
#Autowired
public Db2JdbcTemplate(#Qualifier("datasource2") DataSource ds2) {
super(ds2);
}
}
Modified MyRepository to use those:
package my.app;
#Repository
public class MyRepository {
private Db1JdbcTemplate db1;
private Db2JdbcTemplate db2;
#Autowired
public class MyRepository(Db1JdbcTemplate db1, Db2JdbcTemplate db2) {
this.db1 = db1;
this.db2 = db2;
}
}
So I have #Component annotations on both of those so I expected to be able to be registered and be able to be Autowired. However when I run things I get the following exception:
Error creating bean with name 'repository' defined in file
[/Users/me/spring/target/classes/my/app/MyRepository.class]:
Unsatisfied dependency expressed through constructor parameter 0;
nested exception is
org.springframework.beans.factory.NoSuchBeanDefinitionException: No
qualifying bean of type 'my.app.Db1JdbcTemplate' available: expected
at least 1 bean which qualifies as autowire candidate. Dependency
annotations: {}
Any ideas what I need to change or how I can debug further? I can always go back to the working version but I'd prefer to be more explicit when possible so the compiler can help me out instead of runtime errors caused by a typo'd Qualifier name.
A more standard way of doing this and avoid typos in qualifier names is to avoid using qualifier names, and instead use Qualifier annotations:
#Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Qualifier
public #interface DB1 {
}
#Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Qualifier
public #interface DB2 {
}
and then, when defining your beans:
#Configuration
#ComponentScan
public class AppConfig {
#Bean
#ConfigurationProperties(prefix = "db1.datasource")
#DB1
public DataSource dataSource1() {
return DataSourceBuilder.create().build();
}
#Bean
#DB1
public JdbcTemplate db1(#DB1 DataSource ds) {
return new JdbcTemplate(ds);
}
#Bean
#ConfigurationProperties(prefix = "db2.datasource")
#DB2
public DataSource dataSource2() {
return DataSourceBuilder.create().build();
}
#Bean
#DB2
public JdbcTemplate db1(#DB2 DataSource ds) {
return new JdbcTemplate(ds);
}
}
And finally when injecting your beans:
public class MyRepository(#DB1 JdbcTemplate db1, #DB2 JdbcTemplate db2) {
...
}
If the main concern is typos, just define your names as constants (something like public static final String DATASOURCE1 = "datasource1";) and consistently use those in your qualifier annotations instead of string literals. No need to add new classes or interfaces.
Related
I'm trying to truncate a table with jpa when having multiple datasources configuartion, the command seems to be committed but actually- the data from the table is still persists and not deleted.
My code snippets below:
relevant datasource configuration
#Configuration
#EnableJpaRepositories(basePackages = "com.haaretz.jobs.newsletter_preferences_job.data.newsletter_alerts")
public class NewsletterAlertsConfig {
private Environment environment;
#Autowired
public NewsletterAlertsConfig(Environment environment){
this.environment = environment;
}
#Primary
#Bean(name = "newsletterAlertsDataSource")
public DataSource newsletterAlertsDataSource() {
return DataSourceBuilder
.create()
.driverClassName(environment.getProperty("datasource.newsletterAlerts.driverClassName"))
.url(environment.getProperty("datasource.newsletterAlerts.jdbcUrl"))
.username(environment.getProperty("datasource.newsletterAlerts.username"))
.password(environment.getProperty("datasource.newsletterAlerts.password"))
.build();
}
#Primary
#Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder builder,
#Qualifier("newsletterAlertsDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.haaretz.jobs.newsletter_preferences_job.data.newsletter_alerts")
.persistenceUnit("newsletterAlerts")
.build();
}
}
second datasource configured
#Configuration
#EnableJpaRepositories(basePackages = "com.haaretz.jobs.newsletter_preferences_job.data.gstat",
entityManagerFactoryRef = "gstatEntityManagerFactory")
public class GstatConfig {
private Environment environment;
#Autowired
public GstatConfig(Environment environment){
this.environment = environment;
}
#Bean(name = "gstatDataSource")
public DataSource gstatDataSource() {
return DataSourceBuilder
.create()
.driverClassName(environment.getProperty("datasource.gstat.driverClassName"))
.url(environment.getProperty("datasource.gstat.jdbcUrl"))
.username(environment.getProperty("datasource.gstat.username"))
.password(environment.getProperty("datasource.gstat.password"))
.build();
}
#Bean(name = "gstatEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean gstatEntityManagerFactory(
EntityManagerFactoryBuilder builder,
#Qualifier("gstatDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.haaretz.jobs.newsletter_preferences_job.data.gstat")
.persistenceUnit("gstat")
.build();
}
}
repository
#Repository
public interface NewsletterAlertsRepository extends CrudRepository<NewsletterOpPersonalNl, String> {
#Modifying
#Transactional(propagation = Propagation.REQUIRES_NEW)
#Query(
value = "truncate table OP_PERSONAL_NL",
nativeQuery = true
)
void truncate();
}
the job that activates the deletion
#Component
public class NewsletterPreferencesJob {
private NewsletterAlertsRepository newsletterAlertsRepository;
#Autowired
public NewsletterPreferencesJob(NewsletterAlertsRepository newsletterAlertsRepository) {
this.newsletterAlertsRepository = newsletterAlertsRepository;
}
#Transactional
public void doTask() {
newsletterAlertsRepository.truncate();
}
}
I've also tried to use crudRepoditory default function, which returns the same result (commited but not actually deleted):
newsletterAlertsRepository.deleteAll();
or using a transaction manager, which also doesn't actually deletes the data
TransactionTemplate transactionTemplate = new TransactionTemplate(platformTransactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
#Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
entityManager.createNativeQuery("truncate table OP_PERSONAL_NL").executeUpdate();
}
});
I am creating simple spring boot application. I used spring transaction management to handle the transaction. Here is my code.
ServiceImpl class
#Service("orderService")
public class OrderServiceImpl implements OrderService {
#Autowired
private CustomerDao customerDao;
#Autowired
private OrderDao orderDao;
#Transactional(rollbackFor = Exception.class)
#Override
public Long placeOrder(OrderPlacementRequest request) {
customerDao.save(request.getCustomer());
return orderDao.placeOrder(request.getOrder());
}
}
OrderDaoImpl class,
#Repository("orderDao")
public class OrderDaoImpl extends AbstractHibernateDao implements OrderDao {
#Override
public Long placeOrder(Order order) {
throw new RuntimeException("Test Error Message");
}
}
Configuration class,
#Configuration
#EnableTransactionManagement
public class HibernateConfig {
//Other Configurations
#Autowired
private final Environment environment;
#Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(environment.getRequiredProperty("spring.datasource.driver-class-name"));
dataSource.setUrl(environment.getRequiredProperty("spring.datasource.url"));
dataSource.setUsername(environment.getRequiredProperty("spring.datasource.username"));
dataSource.setPassword(environment.getRequiredProperty("spring.datasource.password"));
return dataSource;
}
#Bean
public DataSourceTransactionManager transactionManager() {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource());
return transactionManager;
}
}
application.properties
spring.aop.proxy-target-class=true
OrderController class
#RestController
#RequestMapping("/order")
public class OrderController {
#Autowired
private OrderService orderService;
#RequestMapping(value = "/place", method = RequestMethod.POST)
public Long placeOrder(#RequestBody OrderPlacementRequest request) {
return orderService.placeOrder(request);
}
}
In my case even though second method placeOrder failed. Customer will be saved in mysql database. What I wanted is to rollback the customer saving method. I went through few articles on transaction management including spring docs and stackoverflow. Still can not find the problem.
Updated with Controller class.
I have a config class that provides two implemenations of the same base bean interface. I want these to be set on the autowired fields conditionally based on an annotation on the field.
public class MyController
{
#Autowired
private MyBeanInterface base;
#Autowired
#MyAnnotation
private MyBeanInterface special;
}
This is a pesudo-code of the config class:
#Configuration
public class ConfigClass
{
#Bean
#Primary
public MyBeanInterface getNormalBeanInterface()
{
return new MyBeanInterfaceImpl();
}
#Bean
//This doesn't work
#ConditionalOnClass(MyAnnotation.class)
public MyBeanInterface getSpecialBeanInterface()
{
return new MyBeanInterfaceForMyAnnotation();
}
}
How can I make the annotated field be populated by the second bean?
Use Qualifier annotation. Example:
Controller:
Add Qualifier annotation at the injected fields with bean id as parameter:
public class MyController
{
#Autowired
#Qualifier("normalBean")
private MyBeanInterface base;
#Autowired
#Qualifier("specialBean")
private MyBeanInterface special;
}
ConfigClass
Specify bean id:
#Configuration
public class ConfigClass
{
#Bean(name="normalBean")
#Primary
public MyBeanInterface getNormalBeanInterface()
{
return new MyBeanInterfaceImpl();
}
#Bean(name="specialBean")
public MyBeanInterface getSpecialBeanInterface()
{
return new MyBeanInterfaceForMyAnnotation();
}
}
I have two datasources that are equal in their structure but not in their Data.
My application has to deal with both of them at the same time.
I have controller, servie, dao structure that looks like this.
Controller Modell:
#Controller
public abstract class MyFancyControllerModell{
private MyService service;
public MyFancyControllerModell (MyService service){
this.service = service;
}
#RequestMapping(....)
public void editSomeData(String data){....}
}
Controller implementation:
#Controller
#RequestMapping(...)
public class MyControllerImpl1 extends MyFancyControllerModell{
#Autowired
public MyControllerImpl1(#Qualifier("myServiceInstance1") MyService service){
super(service);
}
}
#Controller
#RequestMapping(...)
public class MyControllerImpl2 extends MyFancyControllerModell{
#Autowired
public MyControllerImpl2(#Qualifier("myServiceInstance2") MyService service){
super(service);
}
}
And the Service:
public class MyService{
private MyDao myDao;
public MyService(MyDao myDao){
this.myDao = myDao;
}
#Transactional
public void editSomeData(String data){...}
}
I create the Beans in my configuration class like this:
private DataSource lookupDataSource(final String jndiName) {
final JndiDataSourceLookup dsLookup = new JndiDataSourceLookup();
dsLookup.setResourceRef(true);
return dsLookup.getDataSource(jndiName);
}
#Bean
public DataSource dataSource1(){
return lookUpDataSource("dataSource1");
}
#Bean
public DataSource dataSource2(){
return lookUpDataSource("dataSource2");
}
#Bean
public MyService myServiceInstance1(#Qualifier("dataSource1") DataSource dataSource){
return new(MyService(new MyDao(dataSource))));
}
#Bean
public MyService myServiceInstance1(#Qualifier("dataSource1") DataSource dataSource){
return new(MyService(new MyDao(dataSource)));
}
My question is, is it possible to create transactionmanagers for both datasources without the need to declare in the service layer which transactionmanager is used?
I tried creating them as bean just like the services but that did not work.
Check out here the answers for previous quesion related to transaction manager...
https://stackoverflow.com/a/1961853/7504001
I'm trying to save entity in repository but it does not work at all. Repository is Autowired and in runtime I use saveAndFlush to save entity. I'm using PostgreSQL. Above test methods I added comments with explanation what is going on. I expected that method saveAndFlush should work but it did not. I can not find why.
#Transactional
public class TestClass{
#Autowired private MyRepository repository;
#Autowired private EntityManager entityManager;
// Working version
public void writingToRepositoryWorking() {
entityManager.getTransaction().begin();
entityManager.persist(new MyData(99));
entityManager.getTransaction().commit();
}
// not working and throws exception :
// TransactionRequiredException: no transaction is in progress
public void writingToRepositoryNotWorking() {
repository.saveAndFlush(new MyData(99));
}
// not working, no exception, no data in repository,
// but auto generated ID is incremented
public void writingToRepositoryNotWorkingToo() {
repository.save(new MyData(99));
}
}
repository interface file
#Repository
#Transactional
public interface MyRepository extends JpaRepository<MyData, Long> {}
MyData file
#Entity(name = "myData")
public class MyData {
#Id #GeneratedValue(strategy = GenerationType.AUTO) long id;
private int testValue;
public MyData() { }
public BugData(int testValue) {
this.testValue = testValue;
}
public long getId() {
return id;
}
public int getTestValue() {
return testValue;
}
}
ApplicationConfiguration file
#Configuration
#EnableJpaRepositories("com.mypackage.app")
#EnableTransactionManagement
#PropertySource("classpath:application.properties")
#EnableWebMvc
class ApplicationConfiguration extends WebMvcConfigurationSupport {
#Value("${jdbc.url}") private String KEY_JDBC_URL;
#Value("${jdbc.username}") private String KEY_JDBC_USERNAME;
#Value("${jdbc.password}") private String KEY_JDBC_PASSWORD;
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
#Bean
#Autowired
public LocalSessionFactoryBean sessionFactory(DataSource dataSource) {
LocalSessionFactoryBean factory = new LocalSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setPackagesToScan("com.mypackage.app");
factory.setHibernateProperties(hibernateProperties());
return factory;
}
public Properties hibernateProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
properties.setProperty("hibernate.show_sql", "true");
properties.setProperty("hibernate.hbm2ddl.auto", "update");
return properties;
}
#Bean
#Autowired
public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) {
return new HibernateTransactionManager(sessionFactory);
}
#Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.postgresql.Driver");
dataSource.setUrl(KEY_JDBC_URL);
dataSource.setUsername(KEY_JDBC_USERNAME);
dataSource.setPassword(KEY_JDBC_PASSWORD);
return dataSource;
}
#Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource());
em.setPackagesToScan("com.mypackage.app");
em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
em.setJpaProperties(hibernateProperties());
em.afterPropertiesSet();
return em.getObject();
}
#Bean
public EntityManager entityManager(EntityManagerFactory entityManagerFactory) {
return entityManagerFactory.createEntityManager();
}
...
}
For starter, you're actually working on 2 different EntityManager in your non-working test case:
EntityManager autowired into your test by Spring (this one is singleton and should be avoided anyway) ,other is
EntityManager created by the EntityManagerFactory configured in your ApplicationConfiguration.
At the same time, you also have another Session running along side the aforementioned 2 EntityManagers due to your configuration of Hibernate SessionFactory. Additionally, because of the configured HibernateTransactionManager, all transactions created by #Transactional are bound to the Hibernate's Session created by SessionFactory and the EntityManager used by your Repository certainly has no way to know about it. This is why TransactionRequiredException was thrown when your Repository tried to persist data.
To fix it, you may consider removing the Hibernate's SessionFactory and switch the transaction manager to a JpaTransactionManager. Then, #Transactional on your Repository will have the effect of creating a new transaction and binding it to the existing EntityManager that is known to Spring.
One side note is that the #Transactional on your TestClass doesn't help at all as the instance of this class is not instantiated and managed by Spring. To make this work, a proper configuration of transactional test class needs to be provided as described here: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/testing.html.
Hope this helps.