I'm feeling frustrated with my knowledge of spring and java, I hope someone can help me :)
I read the documentation of spring and searched for examples of how to implement multi tenancy in hanacloud platform but no one using spring data/eclipseLink ...
I really don't know how it is the life cycle of objects in framework and how to correctly configure in order to isolate the tenants in spring
The multi tenant strategy adopted was column descriptor to isolate the tenants, this column is described in superclass mapped in child entities...
Each consumer client is identified by url and can be accessed with this API, I've created a route to test the tenant id and the tenant id is different for each URl, which means the subscriptions to my applications its ok...
But the spring data don't isolate the data, i guess he is not creating the entity manager and repositories based in tenant id of consumers and create based in tenant id of provider, the ideia is create only one persistency unit and the data source separate the data based in the column descriptor,
The tenant's ids are generated by the system and its not possible create additional persistency units, they are created in the moment of subscription...
Here's the questions
About the LocalContainerEntityManagerFactoryBean, when he is called in stack ?
It's possible to create one entity manager for each tenant id based in request ?
my configuration class to set datasource
#Configuration
#Profile({ "neo" })
#EnableTransactionManagement
public class DataSourceNeoConfig {
#Bean
public DataSource jndiDataSource() throws IllegalArgumentException, NamingException {
final JndiDataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
DataSource dataSource = dataSourceLookup.getDataSource("java:comp/env/jdbc/DefaultDB");
return dataSource;
}
#Bean
public JpaVendorAdapter jpaVendorAdapter() {
EclipseLinkJpaVendorAdapter jpaVendorAdapter = new EclipseLinkJpaVendorAdapter();
jpaVendorAdapter.setGenerateDdl(true);
jpaVendorAdapter.setShowSql(true);
return jpaVendorAdapter;
}
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean lef = new LocalContainerEntityManagerFactoryBean();
try {
lef.setDataSource(jndiDataSource());
} catch (IllegalArgumentException | NamingException e) {
e.printStackTrace();
}
lef.setJpaVendorAdapter(jpaVendorAdapter());
Properties properties = new Properties();
properties.setProperty("eclipselink.weaving", "false");
properties.setProperty(PersistenceUnitProperties.LOGGING_LEVEL, "FINE");
properties.setProperty("eclipselink.ddl-generation", "create-tables");
// eclipselink.tenant.id is column the identifier in mapped class
// #TenantDiscriminatorColumn(name = "TENANT_ID", contextProperty = "eclipselink.tenant.id", length = 36)
properties.setProperty("eclipselink.tenant.id", TenantHolder.getTenant());
lef.setPackagesToScan("com.strongit.models");
lef.setJpaProperties(properties);
lef.afterPropertiesSet();
return lef;
}
#Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf){
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(emf);
return transactionManager;
}
}
The springBootApplication
#SpringBootApplication
#EnableJpaRepositories(basePackageClasses = AppNameApplication.class, entityManagerFactoryRef = "entityManagerFactory")
public class AppNameApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(AvaliacaoApplication.class, args);
}
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(AvaliacaoApplication.class);
}
}
Thanks in advance !!
Related
This is not about Spring Boot at all.
My English could be better.
Using below Config for Spring Data I'm trying to execute DML requests.
Exactly CrudRepository#save method.
However executing Spring's CrudRepository#save method I'm getting next:
ONLY Selects are logged by hibernate.show_sql feature.
No "insert" or "update" statements are being executed as to hibernate.show_sql logging.
No changes at database at all.
====================================================
Not sure but it looks like a Transaction issue.
Seems that there is no transaction at that point,
so out of transaction CRUD Repos is not able to execute DML requests,
including CrudRepository#save.
Maybe it is something wrong with configuration?
Have a look please and feel free to ask for any additional info.
UPDATE:
The next bad-practice workaround helped me to reach the "Update" statements execution.
//(autowired, shared entity manager)
entityManager.joinTransaction();
repository.save(user);
However it is still a bad practice approach. In this case Spring's purpose is lost.
Anyway it is required for me to use Declarative Code-based Transaction managment.
Question is still open:
What is wrong with my config? #Transactional annotation still doesn't work
User domain entity:
#Data
#EqualsAndHashCode(callSuper = true)
#NoArgsConstructor
#Entity
#Table(name = "users")
public class User
{
#Id
#Column(name = "id_pk", length = 11)
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int idPk;
#Column(name = "user_id", length = 25, nullable = false, unique = true)
private String userId;
#Column(name = "email_addr", length = 120)
private String email;
}
Domain-specific Spring Data CRUD Repository declaration:
public interface UserRepository extends CrudRepository<User, Integer> {
//nothing specific
}
Spring (Boot-less) Code-based configuration:
#EnableJpaRepositories(basePackages = "***",
transactionManagerRef = "jpaTransactionManager")
#EnableTransactionManagement
public class DataConfig
{
#Bean
public EntityManagerFactory entityManagerFactory()
{
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setDataSource(dataSource());
factory.setPackagesToScan(DOMAIN_ENTITY_SCAN_PACKAGE);
factory.setJpaVendorAdapter(getVendorAdapter());
factory.afterPropertiesSet();
return factory.getObject();
}
private HibernateJpaVendorAdapter getVendorAdapter()
{
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setShowSql(Boolean.TRUE);
return vendorAdapter;
}
#Bean
public JpaTransactionManager jpaTransactionManager()
{
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory());
txManager.afterPropertiesSet();
return txManager;
}
}
Finally, I've found a solution for my case.
Since I'm using Spring without its Boot part
I had to configure custom WebApplicationInitializer to let Spring manage application entry point:
public class MainWebAppInitializer implements WebApplicationInitializer
{
#Override
public void onStartup(final ServletContext sc)
{
AnnotationConfigWebApplicationContext root = new AnnotationConfigWebApplicationContext();
root.register(WebAppConfiguration.class, DataConfig.class);
sc.addListener(new ContextLoaderListener(root));
...other not related code ommited
}
}
So because I've registered both Config Classes (WebAppConfiguration.class, DataConfig.class) using AnnotationConfigWebApplicationContext#register
I thought annotating configs with #Configuration would be Redundand.
And I was wrong at that point.
To register TransactionManager correctly you SHOULD annotate your Jpa Config class with #Configuration.
Thus I've managed to annotate my config classes with #Configuration and this solved my issue.
Now Spring CRUD Repositories are able to run DML queries to DB (with help of #save methods).
Precisely talking: now Repositories are able to open own transactions and run required queries in terms of these transactions.
I didnot get this, how you are initializing the datasource properties ? I could not see in your given code.
factory.setDataSource(dataSource());
You should be designing you Configuration class like below. Use both the :
entityManagerFactoryRef="entityManagerFactory",
transactionManagerRef="jpaTransactionManager"
Read the hibernate properties from a yaml or properties file.
And set your datasource
Configuration class : DataConfig
/**
* #author Som
*
*/
#Configuration
#EnableJpaRepositories(basePackages="package-name",
entityManagerFactoryRef="entityManagerFactory",
transactionManagerRef="jpaTransactionManager")
#EnableTransactionManagement
#PropertySource(value = { "classpath:application.yml" })
public class DataConfig {
#Autowired
private Environment environment;
#Value("${datasource.myapp.maxPoolSize:10}")
private int maxPoolSize;
/**
* Populate DataSourceProperties object directly from application.yml
* *
*/
#Bean
#Primary
#ConfigurationProperties(prefix = "datasource.myapp")
public DataSourceProperties dataSourceProperties(){
return new DataSourceProperties();
}
/**
* Configure HikariCP pooled DataSource.
*
*/
#Bean
public DataSource dataSource() {
DataSourceProperties dataSourceProperties = dataSourceProperties();
HikariDataSource dataSource = (HikariDataSource) DataSourceBuilder
.create(dataSourceProperties.getClassLoader())
.driverClassName(dataSourceProperties.getDriverClassName())
.url(dataSourceProperties.getUrl())
.username(dataSourceProperties.getUsername())
.password(dataSourceProperties.getPassword())
.type(HikariDataSource.class)
.build();
dataSource.setMaximumPoolSize(maxPoolSize);
return dataSource;
}
/**
* Entity Manager Factory setup.
*
*/
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws NamingException {
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(dataSource());
factoryBean.setPackagesToScan(new String[] { "package-name" });
factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
factoryBean.setJpaProperties(jpaProperties());
return factoryBean;
}
/**
* Provider specific adapter.
*
*/
#Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
return hibernateJpaVendorAdapter;
}
/**
* Hibernate properties.
*
*/
private Properties jpaProperties() {
Properties properties = new Properties();
properties.put("hibernate.dialect", environment.getRequiredProperty("datasource.myapp.hibernate.dialect"));
properties.put("hibernate.hbm2ddl.auto", environment.getRequiredProperty("datasource.myapp.hibernate.hbm2ddl.method"));
properties.put("hibernate.show_sql", environment.getRequiredProperty("datasource.myapp.hibernate.show_sql"));
properties.put("hibernate.format_sql", environment.getRequiredProperty("datasource.myapp.hibernate.format_sql"));
if(StringUtils.isNotEmpty(environment.getRequiredProperty("datasource.myapp.defaultSchema"))){
properties.put("hibernate.default_schema", environment.getRequiredProperty("datasource.myapp.defaultSchema"));
}
return properties;
}
#Bean
#Autowired
public PlatformTransactionManager jpaTransactionManager(EntityManagerFactory emf) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(emf);
return txManager;
}
}
application.yaml
server:
port: 8081
servlet:
context-path: /CRUDApp
---
spring:
profiles: local,default
datasource:
myapp:
url: jdbc:h2:~/test
username: SA
password:
driverClassName: org.h2.Driver
defaultSchema:
maxPoolSize: 10
hibernate:
hbm2ddl.method: create-drop
show_sql: true
format_sql: true
dialect: org.hibernate.dialect.H2Dialect
---
My current project needs to connect to multiple databases. I set
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
in application.properties.
and I have some dbConfig as below:
#Configuration
public class DBSourceConfiguration {
public final static String DATA_SOURCE_PRIMARY = "dataSource";
public final static String DATA_SOURCE_PROPERTIES = "propertiesDataSource";
public final static String DATA_SOURCE_REPORT = "reportDataSource";
public final static String DATA_SOURCE_NEW_DRAGON = "newDragonDataSource";
#Primary
#Bean(name = DATA_SOURCE_PRIMARY)
#ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
#Bean(name = DATA_SOURCE_REPORT)
#ConfigurationProperties(prefix = "externaldatasource.report")
public DataSource reportDataSource() {
return DataSourceBuilder.create().build();
}
#Bean(name = DATA_SOURCE_NEW_DRAGON)
#ConfigurationProperties(prefix = "externaldatasource.newdragon")
public DataSource newDragonDataSource() {
return DataSourceBuilder.create().build();
}
#Bean(name = DATA_SOURCE_PROPERTIES)
#ConfigurationProperties(prefix = "externaldatasource.properties")
public DataSource propertiesDataSource() {
return DataSourceBuilder.create().build();
}
}
and
<!-- language: Java -->
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = PrimaryDbConfig.ENTITY_MANAGER_FACTORY,
transactionManagerRef = PrimaryDbConfig.TRANSACTION_MANAGER,
basePackageClasses = { _TbsRepositoryBasePackage.class })
public class PrimaryDbConfig extends AbstractDbConfig {
public final static String ENTITY_MANAGER_FACTORY = "entityManagerFactoryPrimary";
public final static String ENTITY_MANAGER = "entityManagerPrimary";
public final static String TRANSACTION_MANAGER = "transactionManagerPrimary";
#Autowired
#Qualifier(DBSourceConfiguration.DATA_SOURCE_PRIMARY)
private DataSource dataSource;
#Primary
#Bean(name = ENTITY_MANAGER_FACTORY)
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder) {
return builder.dataSource(dataSource).properties(getVendorProperties(dataSource)).packages(_TbsEntityBasePackage.class).persistenceUnit("primaryPersistenceUnit").build();
}
#Primary
#Bean(name = ENTITY_MANAGER)
public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
return entityManagerFactory(builder).getObject().createEntityManager();
}
#Primary
#Bean(name = TRANSACTION_MANAGER)
public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder) {
return new JpaTransactionManager(entityManagerFactory(builder).getObject());
}
}
and
<!-- language: Java -->
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = PropertiesDbConfig.ENTITY_MANAGER_FACTORY,
transactionManagerRef = PropertiesDbConfig.TRANSACTION_MANAGER,
basePackageClasses = { _PropertiesRepositoryBasePackage.class })
public class PropertiesDbConfig extends AbstractDbConfig {
public final static String ENTITY_MANAGER_FACTORY = "entityManagerFactoryProperties";
public final static String ENTITY_MANAGER = "entityManagerProperties";
public final static String TRANSACTION_MANAGER = "transactionManagerProperties";
#Autowired
#Qualifier(DBSourceConfiguration.DATA_SOURCE_PROPERTIES)
private DataSource dataSource;
#Bean(name = ENTITY_MANAGER_FACTORY)
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder) {
return builder.dataSource(dataSource).properties(getVendorProperties(dataSource)).packages(_PropertiesEntityBasePackage.class).persistenceUnit("propertiesPersistenceUnit").build();
}
#Bean(name = ENTITY_MANAGER)
public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
return entityManagerFactory(builder).getObject().createEntityManager();
}
#Bean(name = TRANSACTION_MANAGER)
public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder) {
return new JpaTransactionManager(entityManagerFactory(builder).getObject());
}
}
and two more DBConfig classes(just like two DbConfig classes above).
My problem is every time I run this web application, Entities (under different packages) will generate to all databases. In other words, Tbs's(Primary) entities will generate tables to newDragon and all other databases.
For instance, Entity A belongs to primary data source, Entity B belongs to properties datasouce. But framework generates table A, B to both primary database and newDragon database and other two database.
Update 2018/06/01 - 1
Although framework generate all entities to all databases, but I can still access tables from the right database. All my web application functionalities work very well. This is very odd, isn't it?
I guess my configuration is fine, so that there is no any problems while my application access database (like read from wrong database and get empty result or insert data to wrong database, etc). Probably something else cause this gernerte all tables to all databases problem.
Based on the configuration you provided, CRUD tables from the right database shouldn't be problem. But generating tables into the right database, sometimes you may want to check whether the configuration picks entity/package names correctly or not.
Each LocalContainerEntityManagerFactoryBean is set with unique package class, the framework will then scan entities under this package name and generate tables accordingly at target datasource; however, there's a situation the packageToScan will be changed. As you have #EntityScan annotation, it would
overrides packagesToScan on all defined LocalContainerEntityManagerFactoryBean, reference code as follow: EntityScanRegistrar.java
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof LocalContainerEntityManagerFactoryBean) {
LocalContainerEntityManagerFactoryBean factoryBean = (LocalContainerEntityManagerFactoryBean) bean;
factoryBean.setPackagesToScan(this.packagesToScan);
this.processed = true;
}
return bean;
}
As a result, even you have provided each LocalContainerEntityManagerFactoryBean with unique package class, the final result may still be overriden by the framework if you've #EntityScan somewhere at your application. Your configuration seems ok to me, so try to find and resolve the package names between #EntityScan and LocalContainerEntityManagerFactoryBean first, it should solve the issue.
reference: https://github.com/spring-projects/spring-boot/issues/6830
Update 1 (scroll down)
The setup is as follows:
Our application database is constructed and used by two separate users:
SCHEMA - User that has authority to create and grant permissions on tables and
APP - User who is granted permissions (INSERT, UPDATE, DELETE, SELECT) (by SCHEMA) for above tables to be used.
This enables us to lock any schema changes until needed so no profound changes happen through the app user.
I am running integration tests with a live Oracle database that contains both these users. on the class itself, I use the #SqlConfig(dataSource = "schemaDataSource", transactionManager = "transactionManagerSchema").
On the test method I place two #Sql that fail because in the SqlScriptsTestExecutionListener class, the transaction is not managing the same datasource. (hence the error message further below).
I've tried setting the datasource to the transaction manager manually as shown in my config class below, however some unknown process seems to override it every time. (My best guess is through the #DataJpaTest annotation but I don't know exactly which of the 11 Auto Configurations does it, as you can see I've already disabled a couple with no effect).
Test Class:
#RunWith(SpringRunner.class)
#DataJpaTest(excludeAutoConfiguration = {TestDatabaseAutoConfiguration.class, DataSourceAutoConfiguration.class})
#FlywayTest
#SqlConfig(dataSource = TestDataSourceConfig.SCHEMA_DATA_SOURCE, transactionManager = "transactionManagerSchema")
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = {TestDataSourceConfig.class, TestFlywayConfig.class})
#EntityScan(basePackageClasses = BaseEnum.class)
public class NotificationTypeEnumTest {
#Autowired
private EntityManager em;
#Test
#Sql(statements = {"INSERT INTO MYAPP_ENUM (ENUM_ID, \"TYPE\", \"VALUE\") VALUES (MYAPP_ENUM_ID_SEQ.nextval, '" + NotificationTypeEnum.DTYPE + "', 'foo')"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
#Sql(statements = {"DELETE FROM MYAPP_ENUM"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public void canFetchNotificationTypeEnum() throws Exception {
TypedQuery<NotificationTypeEnum> query = em.createQuery("select a from NotificationTypeEnum a", NotificationTypeEnum.class);
NotificationTypeEnum result = query.getSingleResult();
assertEquals("foo", result.getValue());
assertEquals(NotificationTypeEnum.DTYPE, result.getConfigType());
}
}
DataSource and TM config:
#Slf4j #Configuration #EnableTransactionManagement
public class TestDataSourceConfig {
public static final String SCHEMA_DATA_SOURCE = "schemaDataSource";
public static final String SCHEMA_TRANSACTION_MANAGER = "schemaTransactionManager";
/*Main Datasource and supporting beans*/
#Bean #Primary #ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() { return new DriverManagerDataSource(); }
#Bean #Primary #Autowired
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) { return new JpaTransactionManager(emf); }
#Bean(name = SCHEMA_DATA_SOURCE) #ConfigurationProperties(prefix = "myapp.datasource.test_schema")
public DataSource schemaDataSource() { return new DriverManagerDataSource(); }
#Bean(name = SCHEMA_TRANSACTION_MANAGER) #Autowired
public PlatformTransactionManager transactionManagerSchema(#Qualifier(SCHEMA_DATA_SOURCE) DataSource dataSource) {
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
jpaTransactionManager.setDataSource(dataSource);
return jpaTransactionManager;
}
}
The full error that I couldn't fit in the title is:
java.lang.IllegalStateException: Failed to execute SQL scripts for test context
...
SOME LONG STACK TRACE
...
the configured DataSource [org.springframework.jdbc.datasource.DriverManagerDataSource] (named 'schemaDataSource') is not the one associated with transaction manager [org.springframework.orm.jpa.JpaTransactionManager] (named 'transactionManagerSchema').
When there is a single DataSource, it appears the Spring auto-configuration model works fine, however, as soon as there are 2 or more, the assumptions break down and the programmer needs to manually fill in the sudden (plentiful) gaps in configuration required.
Am I missing some fundamental understanding surrounding DataSources and TransactionManagers?
Update 1
After some debugging, I have discovered the afterPropertiesSet() method is being called on the bean I created when the TransactionManager is retrieved for use with the #Sql script annotation. This causes whatever EntityManagerFactory it owns (i.e. JpaTransactionManager.entityManagerFactory) to set the datasource according to its configured EntityManagerFactoryInfo.getDataSource(). The EntityManagerFactory itself is being set as a result of the JpaTransactionManager.setBeanFactory method being called (as it implements BeanFactoryAware).
here is the spring code:
// JpaTransactionManager.java
#Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (getEntityManagerFactory() == null) {
if (!(beanFactory instanceof ListableBeanFactory)) {
throw new IllegalStateException("Cannot retrieve EntityManagerFactory by persistence unit name " +
"in a non-listable BeanFactory: " + beanFactory);
}
ListableBeanFactory lbf = (ListableBeanFactory) beanFactory;
setEntityManagerFactory(EntityManagerFactoryUtils.findEntityManagerFactory(lbf, getPersistenceUnitName()));
}
}
I then tried creating my own EntityManagerFactory bean to attempt to inject it into my created transaction manager but this seems to be opening up Hibernate Specific classes and I wish to stay abstracted at the JPA level. As well as it being difficult to configure at first glance.
Finally, a JPA only solution!
The Solution was to control the creation of the EntityManagerFactoryBeans using the provided spring EntityManagerFactoryBuilder component and inject the EntityManager into the test using the #PersistenceContext annotation.
#SqlConfig(dataSource = TestDataSourceConfig.SCHEMA_DATA_SOURCE, transactionManager = SCHEMA_TRANSACTION_MANAGER, transactionMode = SqlConfig.TransactionMode.ISOLATED)
...
public class MyJUnitTest {
#PersistenceContext(unitName = "pu")
private EntityManager em;
...
#Test
#Sql(statements = {"SOME SQL USING THE PRIVILEGED SCHEMA CONNECTION"}, ...)
public void myTest() {
em.createQuery("...").getResultList() // uses the APP database user.
}
}
Below is the configuration for both datasources. The application related DataSource beans all have #Primary in their definition to disambiguate any #Autowired dependencies. there are no Hibernate specific classes needed other than the Automatic hibernate config done through the #DataJpaTest class.
#Configuration
#EnableTransactionManagement
#EnableConfigurationProperties(JpaProperties.class)
public class TestDataSourceConfig {
public static final String SCHEMA_DATA_SOURCE = "schemaDS";
public static final String SCHEMA_TRANSACTION_MANAGER = "schemaTM";
public static final String SCHEMA_EMF = "schemaEMF";
/*Main Datasource and supporting beans*/
#Bean
#Primary
#ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return new DriverManagerDataSource();
}
#Bean #Primary #Autowired
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) { return new JpaTransactionManager(emf); }
#Bean #Primary
public LocalContainerEntityManagerFactoryBean emfBean(
EntityManagerFactoryBuilder entityManagerFactoryBuilder,
DataSource datasource,
JpaProperties jpaProperties) {
return entityManagerFactoryBuilder
.dataSource(datasource)
.jta(false)
.packages(CourseOffering.class)
.persistenceUnit("pu")
.properties(jpaProperties.getProperties())
.build();
}
#Bean(name = SCHEMA_EMF)
public LocalContainerEntityManagerFactoryBean emfSchemaBean(
EntityManagerFactoryBuilder entityManagerFactoryBuilder,
#Qualifier(SCHEMA_DATA_SOURCE) DataSource schemaDataSource,
JpaProperties jpaProperties) {
return entityManagerFactoryBuilder
.dataSource(schemaDataSource)
.jta(false)
.packages(CourseOffering.class)
.persistenceUnit("spu")
.properties(jpaProperties.getProperties())
.build();
}
#Bean(name = SCHEMA_DATA_SOURCE)
#ConfigurationProperties(prefix = "myapp.datasource.test_schema")
public DataSource schemaDataSource() { return new DriverManagerDataSource(); }
#Bean(name = SCHEMA_TRANSACTION_MANAGER)
public PlatformTransactionManager transactionManagerSchema(
#Qualifier(SCHEMA_EMF) EntityManagerFactory emfSchemaBean) {
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
jpaTransactionManager.setEntityManagerFactory(emfSchemaBean);
return jpaTransactionManager;
}
}
Actual Test Class:
#RunWith(SpringRunner.class) // required for all spring tests
#DataJpaTest(excludeAutoConfiguration = {TestDatabaseAutoConfiguration.class, DataSourceAutoConfiguration.class}) // this stops the default data source and database being configured.
#SqlConfig(dataSource = TestDataSourceConfig.SCHEMA_DATA_SOURCE, transactionManager = SCHEMA_TRANSACTION_MANAGER, transactionMode = SqlConfig.TransactionMode.ISOLATED) // make sure the #Sql statements are run using the SCHEMA datasource and txManager in an isolated way so as not to cause problems when running test methods requiring these statements to be run.
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = {TestDataSourceConfig.class})
#TestExecutionListeners({
SqlScriptsTestExecutionListener.class, // enables the #Sql script annotations to work.
SpringBootDependencyInjectionTestExecutionListener.class, // injects spring components into the test (i.e. the EntityManager)
TransactionalTestExecutionListener.class}) // I have this here even though the #Transactional annotations don't exist yet as I plan on using them in further tests.
public class NotificationTypeEnumTest {
#PersistenceContext(unitName = "pu") // required to inject the correct EntityManager
private EntityManager em;
// these statements are
#Test
#Sql(statements = {"INSERT INTO MYAPP_ENUM (ENUM_ID, \"TYPE\", \"VALUE\") VALUES (MYAPP_ENUM_ID_SEQ.nextval, '" + NotificationTypeEnum.DTYPE + "', 'foo')"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
#Sql(statements = {"DELETE FROM MYAPP_ENUM"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public void canFetchNotificationTypeEnum() throws Exception {
TypedQuery<NotificationTypeEnum> query = em.createQuery("select a from NotificationTypeEnum a", NotificationTypeEnum.class); // notification type is just a subclass of the BaseEnum type
NotificationTypeEnum result = query.getSingleResult();
assertEquals("foo", result.getValue());
assertEquals(NotificationTypeEnum.DTYPE, result.getConfigType());
}
}
noteworthy classes:
EntityManagerFactoryBuilder - I don't like factory factories, but this one served me well in creating the correct implementation of EntityManagerFactory without depending on any hibernate specific classes. may be injected with #Autowired. The builder bean itself is configured through the HibernateJpaAutoConfiguration class (extends JpaBaseConfiguration) (imported by #DataJpaTest).
JpaProperties - useful for maintaining application.properties config in the resulting entitymanagerfactories. enabled through the #EnableConfigurationProperties(JpaProperties.class) annotation above this config class.
#PersistenceContext(unitName = "...") - I can inject the correct EntityManager in my test class with this annotation.
I'm using Spring and JPA repositories, with multiple database schemas for different entities, and configuring each of them with the proper values and independent beans:
#Configuration
#EnableJpaRepositories(basePackageClasses = {ClassFromSchemaXRepository.class}, entityManagerFactoryRef = "ClassFromSchemaXEntityManagerFactoryA", transactionManagerRef = "ClassFromSchemaXTransactionManager")
{
#Bean
public DataSource classFromSchemaXDataSource(){
HikariDataSource hds = new HikariDataSource();
hds.setJdbcUrl(env.getProperty("dataSource.jdbcUrl.schema.x"));
hds.setUsername(env.getProperty("dataSource.user"));
hds.setPassword(env.getProperty("dataSource.password"));
//...
}
#Bean
public LocalContainerEntityManagerFactoryBean classFromSchemaXEntityManagerFactory() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(classFromSchemaXDataSource());
em.setPackagesToScan(new String[] { "com.company.core.domain.x" });
//...
}
#Bean
public PlatformTransactionManager classFromSchemaXTransactionManager() {
//...
}
}
This configuration works as expected, and the entire set of entities which are located under the 'com.company.core.domain.x' package is mapped to this sets of beans with the schema connection string which is defined in env.getProperty("dataSource.jdbcUrl.schema.x")
However, I am now trying to configure a specific entity to be used by changeable schema and therefor dymanic DataSource/EntityManagerFactory/TransactionManager.
The business logic should determine which schema should be used at runtime.
What is the best method of doing that?
I've found what I was looking for which is extending the AbstractRoutingDataSource class, and overriding the determineCurrentLookupKey() method
I have a Spring Boot app for which I have configured two data sources. So far I've configured the data sources in my Application class (annotated with #EnableAutoConfiguration):
#Bean
#Primary
#ConfigurationProperties(prefix="datasource.db1")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
#Bean
#ConfigurationProperties(prefix="datasource.db2")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
I also added the configuration values to application.properties:
datasource.db1.url=...
...
datasource.db2.url=...
...
Since db1 is the #Primary data source, it is chosen by default. How do I tell an interface extending JpaRepository that it should use db2 instead?
UPDATE: mentioning that my repository is an interface.
You can get the bean associated to the secondary data source from the application context.
For example in Application.java (I'm also using Spring Boot) you define:
#Bean
#ConfigurationProperties(prefix="datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
and in a service (here for calling a stored procedure) you have:
#Service
public class EngineImpl implements EngineDao {
private SetScartiProcedure setScarti;
#Autowired
public void init(ApplicationContext ctx) {
DataSource dataSource = (DataSource) ctx.getBean("secondaryDataSource");
this.setScarti = new SetScartiProcedure(dataSource);
}
public class SetScartiProcedure extends StoredProcedure {
...
}
based on this you can define several DataSourcethis way
#Bean
public LocalContainerEntityManagerFactoryBean customerEntityManagerFactory(
EntityManagerFactoryBuilder builder) {
return builder
.dataSource(customerDataSource())
.packages(Customer.class)
.persistenceUnit("customers")
.build();
}
#Bean
public LocalContainerEntityManagerFactoryBean orderEntityManagerFactory(
EntityManagerFactoryBuilder builder) {
return builder
.dataSource(orderDataSource())
.packages(Order.class)
.persistenceUnit("orders")
.build();
}
and then bind each one of them to different classes that each one of them manages
#Configuration
#EnableJpaRepositories(basePackageClasses = Customer.class,
entityManagerFactoryRef = "customerEntityManagerFactory")
public class CustomerConfiguration {
...
}
#Configuration
#EnableJpaRepositories(basePackageClasses = Order.class,
entityManagerFactoryRef = "orderEntityManagerFactory")
public class OrderConfiguration {
...
}
the repositories should know which database to use by the DataSource that was bidden to the class