Database Configuration with Spring 3.2 Environment Profiles
This is a followup to my previous blog Spring 3.1 Environment Profiles
Let’s demonstrate how to configure an application to use different databases based on configuration. The code below is using Spring JavaConfig, in lieu of XML config.
Source Code: Spring Data Demos with Profile Example
Profiles
Spring 3.2 has improved the environment-aware profiles feature. Our applications can activate beans, at runtime, defined in specific profiles. For example to test different databases, we can use profiles such as Oracle, MySQL, HSQL, etc.
Let’s see how to configure a Spring application for multiple database vendor support to aid developers in testing with an in memory database (offline) before connecting to the enterprise database.
Common JPA Configuration
Since all of the classes will use JPA and Hibernate in these examples, there is clearly common configuration for all database vendors.
Common beans are typically DataSource
, TransactionManager
, EntityManager
and EntityManagerFactory
.
We’ll create configuration classes for the different database types in the package com.gordondickens.orm.config
├── db │ ├── JpaCommonConfig.java │ ├── JpaDerbyClientConfig.java │ ├── JpaDerbyEmbeddedConfig.java │ ├── JpaH2EmbeddedConfig.java │ ├── JpaHsqlEmbeddedConfig.java │ ├── JpaMySqlEmbeddedConfig.java │ ├── JpaOracleConfig.java │ ├── JpaOracleJndiConfig.java │ └── JpaPostgresqlConfig.java └── support ├── DatabaseConfigProfile.java └── Hbm2ddlType.java
JpaCommonConfig
@Configuration
– defines this class as a Spring Configuration class@PropertySource
– loads in external properties into theEnvironment
@Bean
– defines a Spring bean, where the bean name is defined by the method name and the type is defined by the return type@Value
– uses SpEL (Spring Expression Language) to extract property values from the autowiredEnvironment
, note the pound sign “#” indicates a bean referencegetDatabaseDialect()
– is required to be implemented in concrete classesgetJpaProperties()
– is expected to be implemented in concrete classes- The getters will provide values from the environment, via the concrete Jpa config classes
package com.gordondickens.orm.config.db; import org.hibernate.dialect.Dialect; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; import javax.persistence.EntityManager; import javax.sql.DataSource; import java.util.Properties; /** * Common Settings for JPA */ @Configuration @PropertySource("classpath:/META-INF/spring/app-config.properties") public abstract class JpaCommonConfig { public static final String UNDEFINED = "**UNDEFINED**"; public static final String CONNECTION_CHAR_SET = "hibernate.connection.charSet"; public static final String VALIDATOR_APPLY_TO_DDL = "hibernate.validator.apply_to_ddl"; public static final String VALIDATOR_AUTOREGISTER_LISTENERS = "hibernate.validator.autoregister_listeners"; @Autowired Environment environment; @Value("#{ environment['entity.package'] }") private String entityPackage = "com.gordondickens.orm.hibernate.domain"; @Bean public abstract DataSource dataSource(); @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); vendorAdapter.setGenerateDdl(true); vendorAdapter.setDatabasePlatform(getDatabaseDialect().getName()); vendorAdapter.setShowSql(true); LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); factory.setJpaVendorAdapter(vendorAdapter); factory.setPackagesToScan(entityPackage); factory.setDataSource(dataSource()); if (getJpaProperties() != null) { factory.setJpaProperties(getJpaProperties()); } return factory; } @Bean public EntityManager entityManger() { return entityManagerFactory().getObject().createEntityManager(); } @Bean public PlatformTransactionManager transactionManager() { JpaTransactionManager txManager = new JpaTransactionManager(); txManager.setEntityManagerFactory(entityManagerFactory().getObject()); return txManager; } protected abstract Class<? extends Dialect> getDatabaseDialect(); protected Properties getJpaProperties() { return null; } public String getDatabaseName() { return environment.getProperty("database.name", UNDEFINED); } public String getHost() { return environment.getProperty("database.host", UNDEFINED); } public String getPort() { return environment.getProperty("database.port", UNDEFINED); } public String getUrl() { return environment.getProperty("database.url", UNDEFINED); } public String getUser() { return environment.getProperty("database.username", UNDEFINED); } public String getPassword() { return environment.getProperty("database.password", UNDEFINED); } public String getDriverClassName() { return environment.getProperty("database.driverClassName", UNDEFINED); } public String getDialect() { return environment.getProperty("database.dialect", UNDEFINED); } public String getDatabaseVendor() { return environment.getProperty("database.vendor", UNDEFINED); } public String getHbm2ddl() { return environment.getProperty("database.hbm2ddl.auto", "none"); } public String getHibernateCharSet() { return environment.getProperty("database.hibernateCharSet", "UTF-8"); } public String getDatabaseValidationQuery() { return environment.getProperty("database.validation.query", UNDEFINED); } }
Concrete Database Configuration Classes
Inherit from the JpaCommonConfig class to provide vendor specific configuration.
package com.gordondickens.orm.config.db; import com.gordondickens.orm.config.support.DatabaseConfigProfile; import com.gordondickens.orm.config.support.Hbm2ddlType; import org.apache.commons.dbcp.BasicDataSource; import org.hibernate.cfg.ImprovedNamingStrategy; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.HSQLDialect; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.PropertySource; import org.springframework.jdbc.datasource.init.DatabasePopulator; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; import javax.sql.DataSource; import java.sql.SQLException; import java.util.Properties; import static java.lang.Boolean.TRUE; import static org.hibernate.cfg.Environment.*; import static org.hibernate.ejb.AvailableSettings.NAMING_STRATEGY; /** * HSQL Embedded */ @Configuration @Profile(DatabaseConfigProfile.HSQL_EMBEDDED) @PropertySource("classpath:/META-INF/spring/hsql.properties") public class JpaHsqlEmbeddedConfig extends JpaCommonConfig { @Override @Bean(destroyMethod = "close") public DataSource dataSource() { BasicDataSource dataSource = new BasicDataSource(); dataSource.setDriverClassName(getDriverClassName()); dataSource.setUrl(getUrl()); dataSource.setUsername(getUser()); dataSource.setPassword(getPassword()); dataSource.setValidationQuery(getDatabaseValidationQuery()); dataSource.setTestOnBorrow(true); dataSource.setTestOnReturn(true); dataSource.setTestWhileIdle(true); dataSource.setTimeBetweenEvictionRunsMillis(1800000); dataSource.setNumTestsPerEvictionRun(3); dataSource.setMinEvictableIdleTimeMillis(1800000); return dataSource; } @Override protected Properties getJpaProperties() { Properties properties = new Properties(); properties.setProperty(HBM2DDL_AUTO, Hbm2ddlType.CREATE_DROP.toValue()); properties.setProperty(GENERATE_STATISTICS, TRUE.toString()); properties.setProperty(SHOW_SQL, TRUE.toString()); properties.setProperty(FORMAT_SQL, TRUE.toString()); properties.setProperty(USE_SQL_COMMENTS, TRUE.toString()); properties.setProperty(CONNECTION_CHAR_SET, getHibernateCharSet()); properties.setProperty(NAMING_STRATEGY, ImprovedNamingStrategy.class.getName()); return properties; } @Override protected Class<? extends Dialect> getDatabaseDialect() { return HSQLDialect.class; } @Bean public DatabasePopulator databasePopulator(DataSource dataSource) { ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); populator.setContinueOnError(true); populator.setIgnoreFailedDrops(true); // populator.addScript(new ClassPathResource("/sql/mydata-dml.sql")); try { populator.populate(dataSource.getConnection()); } catch (SQLException ignored) {} return populator; } }
Testing
JavaConfig allows us to configure Spring with or without XML configuration. If we want to test beans that are defined in a Configuration class we configure our test with the loader
and classes
arguments of the @ContextConfiguration
annotation.
Test Context Configuration
Create a configuration class, bootstrapping the test context for the beans to be tested.
@ComponentScan
– scans for annotated beans and entities. Note that@ComponentScan
will ignore auto-discovery of other@Configuration
classes@EnableJpaRepositories
– configures Spring-Data-JPA repository interfaces annotated with@Repository
@EnableTransactionManagement
– proxies the@Transactional
annotated classes
package com.gordondickens.orm.hibernate.config; import com.gordondickens.orm.hibernate.domain.Employee; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.*; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * Test Configuration */ @Configuration @ComponentScan(basePackages = "com.gordondickens.orm.hibernate", excludeFilters = {@ComponentScan.Filter(Configuration.class)}) @EnableTransactionManagement @EnableJpaRepositories(basePackages = "com.gordondickens.orm.hibernate.repository") public class TestConfig { @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public Employee employee() { return new Employee(); } }
JUnit Test
@ActiveProfiles
– sets the valid profiles for the test execution similar to using the environment variablespring.profiles.active
@ContextConfiguration
– sets up the test context. Here we load in the test bootstrap class and the vendor specific JPA configuration classes
package com.gordondickens.orm.hibernate.domain; import com.gordondickens.orm.config.db.JpaHsqlEmbeddedConfig; import com.gordondickens.orm.config.support.DatabaseConfigProfile; import com.gordondickens.orm.hibernate.config.TestConfig; import com.gordondickens.orm.hibernate.service.EmployeeService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.samePropertyValuesAs; import static org.junit.Assert.assertThat; @ActiveProfiles(DatabaseConfigProfile.HSQL_EMBEDDED) @Transactional @ContextConfiguration(classes = {JpaHsqlEmbeddedConfig.class, TestConfig.class}) @RunWith(SpringJUnit4ClassRunner.class) public class EmployeeHsqlIntegrationTest { @Autowired EmployeeService employeeService; @Test public void testMarkerMethod() { Employee employee = new Employee(); employee.setFirstName("Cletus"); employee.setLastName("Fetus"); employeeService.saveEmployee(employee); assertThat("Employee MUST exist", employee, notNullValue()); assertThat("Employee MUST have PK", employee.getId(), notNullValue()); Employee employee1 = employeeService.findEmployee(employee.getId()); assertThat("Employee Must be Found by ID", employee1.getId(), samePropertyValuesAs(employee.getId())); } }
Summary
Using the Profile feature, we can configure a database so run locally on an embedded database, such as Derby, HSQL, or H2. Using profiles gives developers the ability to validate entity and ORM configuration before connecting to the enterprise database, such as Oracle.
The example could be tuned to Component Scan configuration classes, eliminating the explicit include of the vendor specific JPA config class.