Database Configuration with Spring 3.2 Environment Profiles

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 the Environment
  • @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 autowired Environment, note the pound sign “#” indicates a bean reference
  • getDatabaseDialect() – is required to be implemented in concrete classes
  • getJpaProperties() – 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 variable spring.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.

Sources

Spring Data Demos with Profile Example

About Gordon

Technology enthusiast primarily focused on Java and Open Source projects. Spring Certified Professional and Trainer. http://twitter.com/gdickens http://linkedin.com/in/gordondickens http://github.com/gordonad
This entry was posted in Annotations, Java, JUnit, Spring, Spring Data, Testing and tagged , , , , , , , , , , , , , , , , , , , , , , , , , , . Bookmark the permalink.

2 Responses to Database Configuration with Spring 3.2 Environment Profiles

  1. Gordon, I’m finding answers on your blog far too often.

    As part of a conversion to use Liquibase (from manual schema management), and while part way through conversion to Java configuration, the your SpEL above inspired the following entry in my Hibernate properties:


    <prop key="hibernate.hbm2ddl.auto">#{ environment.acceptsProfiles('liquibase') ? 'none' : environment['hibernate.hbm2ddl'] }</prop>

  2. Speedy says:

    It has to be:

    @Bean
    public EntityManager entityManager() {
    return entityManagerFactory().getObject().createEntityManager();
    }

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>