Spring MVC with basic object persistence using Spring Data JPA and Hibernate

Spring Data Hibernate Example

In an previous post we created a Sample Spring MVC project that didn’t do much except have a simple form. In this example we will add a basic service to process the form input and then save it to the database. For the database I am using MySQL, layered with Hibernate as the object mapper and then Spring Data JPA for abstraction and some nice boilerplate crud operations.

The first step is creating our service to handle the business the logic. I am a firm believer in separation of concerns and the law of demeter. I interpret it here as using Spring MVC for Model=Forms, Views=JSP, Controller=View Processor & link to the business service. This means that NO business logic is in the controller. Only code relevant to rendering the views or models goes here. These three things should be able to maintained by a front-end developer. The business logic could be in one or more services which could either reside in the application or external service accessed via REST or SOAP.

For this case we’ll setup the service in the application and its first purpose is to reject any user named “Dave” (sorry Dave…), then it will convert the form into a DB model for saving. The object ID from the database will be added to the form and returned back for display on the confirmation page.

Create the Service interface:

public interface SampleService {

    public SignupForm saveFrom(SignupForm signupForm) throws InvalidUserException;

}

Create the service implementation:

@Service("sampleService")
public class SampleServiceImpl implements SampleService {

    public SignupForm saveFrom(SignupForm signupForm) throws InvalidUserException{

        String firstName = signupForm.getFirstName();

        if(StringUtils.isEmpty(firstName) || "Dave".equalsIgnoreCase(firstName)) {
            throw new InvalidUserException("Sorry Dave");
        }

		// TODO: Save the form

        return signupForm;
    }
}

Notice the @Service annotation at the top. This is the same as @Component and will automatically be added to the Spring application context at startup. Make sure the component scan includes the base package with your services.

Next add the dependencies for spring data, jpa and hibernate to the pom.xml

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>1.2.0.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.hibernate.javax.persistence</groupId>
    <artifactId>hibernate-jpa-2.0-api</artifactId>
    <version>1.0.1.Final</version>
</dependency>

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>4.1.9.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>4.1.9.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
    <version>4.1.9.Final</version>
</dependency>

<dependency>
      <groupId>com.jolbox</groupId>
      <artifactId>bonecp</artifactId>
      <version>0.7.1.RELEASE</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.22</version>
	<scope>runtime</scope>
</dependency>

JDBC Driver/Connection Pool
BoneCP was added for the JDBC driver and connection pool. This could be swapped out for Apache commons DBCP, C3P0 or any other you prefer.

Next create the DB model and add annotations that will instruct hibernate how to persist the object. Since the form allows a user to signup we will create a user class. This is created in com.luckyryan.sample.dao.model

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class User {

    @Id
    @GeneratedValue
    private Long id;
    
    private String firstName;
    private String lastName;
    private String email;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

Now we need a UserDao to manage the User objects. This is were the cool stuff happens. Spring Data JPA makes this extremely easy. All we need in an interface and it will do the implementation for us. Even better is that there already is a CrudRepository interface that can be extended that will give you the basic operations for free. The UserDao is created in the dao package

import org.springframework.data.repository.CrudRepository;

public interface UserDao extends CrudRepository<User, Integer> {
}
DAO Interface
There is no need to annotate the class. We will set the JPA scan path in the spring config so it will automatically be picked up and added to the application context.

Create a property file for the db settings
jdbc.properties

jdbc.url=${jdbc.url}
jdbc.username=${jdbc.username}
jdbc.password=${jdbc.password}

This above property file is using a reference to pom.xml properties. This is accomplished using the resources plugin. If you get errors with this see the note at the bottom of the page. You could alternatively just specify the properties here if you don’t need to use profile or environment specific settings.

Configure JPA, Hibernate and BoneCP with Spring Data. The XML and Java Config options are listed below, you only need the style relevant to your project, not both.

xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd
           http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.0.xsd">

    <context:annotation-config/>
    <context:component-scan base-package="com.luckyryan.sample"/>

    <!-- BoneCP configuration -->
    <bean id="mainDataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close">
        <property name="driverClass" value="com.mysql.jdbc.Driver" />
        <property name="jdbcUrl" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="idleConnectionTestPeriodInMinutes" value="60"/>
        <property name="idleMaxAgeInMinutes" value="240"/>
        <property name="maxConnectionsPerPartition" value="30"/>
        <property name="minConnectionsPerPartition" value="10"/>
        <property name="partitionCount" value="3"/>
        <property name="acquireIncrement" value="5"/>
        <property name="statementsCacheSize" value="100"/>
        <property name="releaseHelperThreads" value="3"/>
    </bean>

    <!-- SPRING - JPA -->
    <jpa:repositories
            base-package="com.luckyryan.sample.dao" />

    <bean class="org.springframework.orm.jpa.JpaTransactionManager"
          id="transactionManager">
        <property name="entityManagerFactory"
                  ref="entityManagerFactory" />
        <property name="jpaDialect">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" />
        </property>
    </bean>

    <bean id="entityManagerFactory"
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="mainDataSource" />
        <property name="packagesToScan" value="com.luckyryan.sample.dao.model"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="generateDdl" value="true" />
                <property name="showSql" value="false"/>
                <property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect"/>
                <property name="database" value="MYSQL"/>
            </bean>
        </property>
        <property name="jpaProperties">
            <value>
                hibernate.cache.use_second_level_cache = true
                hibernate.cache.region.factory_class = org.hibernate.cache.ehcache.EhCacheRegionFactory
                hibernate.cache.use_query_cache = true
                hibernate.generate_statistics = true
            </value>
        </property>
    </bean>

</beans>

The xml can be included in the project web.xml or as an include in another applcationContext.

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
        classpath:applicationContext-dao.xml
    </param-value>
</context-param>

Java Config

package com.luckyryan.sample.config;


import com.jolbox.bonecp.BoneCPDataSource;
import org.springframework.beans.factory.annotation.Autowired;
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.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.hibernate4.HibernateExceptionTranslator;
import org.springframework.orm.jpa.JpaDialect;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManagerFactory;
import java.util.Properties;

@PropertySource(value = "classpath:db.properties")
@EnableTransactionManagement(proxyTargetClass = true)
@EnableJpaRepositories("com.luckyryan.sample.dao")
@Configuration
public class dbConfig {

    @Autowired
    Environment env;

    @Bean
    public BoneCPDataSource boneCPDataSource() {

        BoneCPDataSource boneCPDataSource = new BoneCPDataSource();
        boneCPDataSource.setDriverClass("com.mysql.jdbc.Driver");
        boneCPDataSource.setJdbcUrl(env.getProperty("jdbc.url"));
        boneCPDataSource.setUsername(env.getProperty("jdbc.username"));
        boneCPDataSource.setPassword(env.getProperty("jdbc.password"));
        boneCPDataSource.setIdleConnectionTestPeriodInMinutes(60);
        boneCPDataSource.setIdleMaxAgeInMinutes(420);
        boneCPDataSource.setMaxConnectionsPerPartition(30);
        boneCPDataSource.setMinConnectionsPerPartition(10);
        boneCPDataSource.setPartitionCount(3);
        boneCPDataSource.setAcquireIncrement(5);
        boneCPDataSource.setStatementsCacheSize(100);
        boneCPDataSource.setReleaseHelperThreads(3);

        return boneCPDataSource;

    }

    @Bean
    public HibernateExceptionTranslator hibernateExceptionTranslator() {
        return new HibernateExceptionTranslator();
    }

    @Bean
    @Autowired
    public EntityManagerFactory entityManagerFactory(BoneCPDataSource dataSource) {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(true);
        vendorAdapter.setShowSql(false);
        vendorAdapter.setDatabasePlatform("org.hibernate.dialect.MySQL5InnoDBDialect");
        vendorAdapter.setDatabase(Database.MYSQL);

        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setPackagesToScan("com.luckyryan.sample.dao.model");
        factory.setDataSource(dataSource);

        Properties properties = new Properties();
        properties.setProperty("hibernate.cache.use_second_level_cache", "true");
        properties.setProperty("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory");
        properties.setProperty("hibernate.cache.use_query_cache", "true");
        properties.setProperty("hibernate.generate_statistics", "true");

        factory.setJpaProperties(properties);

        factory.afterPropertiesSet();

        return factory.getObject();
    }

    @Bean
    @Autowired
    public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        JpaDialect jpaDialect = new HibernateJpaDialect();
        txManager.setEntityManagerFactory(entityManagerFactory);
        txManager.setJpaDialect(jpaDialect);
        return txManager;
    }

}

This file config can be added to the main config with the import annotation.

@Configuration
@Import(dbConfig.class)
public class appConfig {
	...
}

HibernateExceptionTranslator
Notice that a HibernateExceptionTranslator is specified in the Java Config and not in the xml. This was an xml freebie and needs specific declaration for the java config.

The Java Config version takes advantage of the environment feature introduced in Spring 3.1. This is a great feature that allows you to change profiles at runtime which would allow changes to the db properties and which database was being used. In the XML version this still used db.properties and maven resources plugin to set this at build time.

The first part is BoneCP. This is the data source and connection pool that will talk to mysql. We tell BoneCP to use the MySQL driver and set options for pooling and connection settings.

Next tell Spring Data to scan a package for repository interfaces. No specific class annotation is needed for this to work.

<jpa:repositories base-package="com.luckyryan.sample.dao" />
or
@EnableJpaRepositories("com.luckyryan.sample.dao")

We need a transaction manager, give it the entity manager bean and the dialect that we are using.

The entity manger is core. This is were we tell it the data source and that we want to use Hibernate as the JPA implementation.
packagesToScan is the scan path of the annotated models. Instead of annotations you can skip this property and define a mapping file.
jpaVendorAdapter is were we set hibernate. There are properties that we can add here that are good time savers for testing. generateDdl will automatically update the schema to match the declared models at startup. Awesome for testing but I suggest revising this setting in production, sometimes automatic column changes are not desirable. showSql will output the sql string to the log, great for debugging but also something that should not hit production. You will also need to tell hibernate which database you are using so it can map and build the sql appropriately.
jpaProperties is an a generic property list. The XML makes this nice because a key value pair is automatically converted to the correct type where it’s explicit declaration in the java config. For this app we are passing in hibernate properties which also removed the need for a persistence.xml since we already have annotated models and now global properties like caching are declared.

Now we can autowire the Dao into the service and save the user form.

@Service
public class SampleServiceImpl implements SampleService {

    @Autowired
    UserDao userDao;

    public SignupForm saveFrom(SignupForm signupForm) throws InvalidUserException{

        String firstName = signupForm.getFirstName();

        if(StringUtils.isEmpty(firstName) || "Dave".equalsIgnoreCase(firstName)) {
            throw new InvalidUserException("Sorry Dave");
        }

        // Shown for example only, you could use a constructor, builder pattern or Dozer
        // point is that the DAO only knows and cares about users and not any UI form.
        User user = new User();
        user.setFirstName(signupForm.getFirstName());
        user.setEmail(signupForm.getEmail());
        user.setLastName(signupForm.getLastName());

        user = userDao.save(user);

        signupForm.setId(user.getId());

        return signupForm;

    }
}

The above will show that saves the form and the updated signupForm now has an id that we can use in the jsp to build CRUD links. Overall the above method is crap for design but at least shows the concept. I know it a few days I won’t be able to sleep because I wrote this method this way…

Now create a local schema (with what ever name you defined in the property file) and start the app. It will automatically create the table and columns for you. Enter some test data, submit and you will see a new row in the database.

Next steps would be showing the updated form on the completion page or adding an edit method to the form.

Build Error – Stack overflow
If you get a build error because the property file resulting in a stack overflow error. It is probably because the maven resources plugin is trying to replace circular references. To fix it you can update the resources part of the build section of the pom.xml to exclude the applicationContext*.xml files.

<resources>
    <resource>
        <directory>src/main/resources</directory>
        <excludes>
            <exclude>*.xml</exclude>
        </excludes>
        <filtering>true</filtering>
    </resource>
    <resource>
        <directory>src/main/resources</directory>
        <includes>
            <include>*.xml</include>
        </includes>
        <filtering>false</filtering>
    </resource>
</resources>

13 Comments

  1. Sarvan December 15, 2016
  2. Satyam December 1, 2016
  3. CESAR F NORIEGA July 6, 2016
  4. geo December 22, 2015
  5. Mohamed December 28, 2014
  6. Michael August 6, 2014
  7. nerny June 16, 2014
    • Ryan June 20, 2014
  8. Nathania Heimoski May 2, 2014
  9. Atif November 24, 2013
  10. rijad May 2, 2013
    • Ryan May 2, 2013

Leave a Reply to Sarvan Cancel reply

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