CRUD Operations with Spring Data JPA
Java, Maven, Hibernate, JPA, Spring Data, Spring
Spring Data JPA provides a familiar and consistent, Spring-based programming model for data access while still retaining the special traits of the underlying data store.
It makes it easy to use data access technologies, relational and non-relational databases, map-reduce frameworks, and cloud-based data services.
Introduction
This is the fifth part part of a series of posts focused on Hibernate
and JPA
. In my previous post we saw how to abstract away
most of the logic written in prior post with Spring. In this post we will
look at abstracting away the CRUD layer to Spring Data JPA and writing less boilerplate code.
We will end this post by externalizing our database configuration to a .properties
file.
Prerequisites
Project Structure
This is a build up on previous posts and our folder structure will remain relatively the same:
.
|__src/
| |__main/
| | |__java/
| | | |__com/
| | | | |__tutorial/
| | | | | |__Application.java
| | | | | |__entity/
| | | | | | |__Person.java
| | | | | |__repository/
| | | | | | |__PersonRepository.java
| | |__resources/
| | | |__application.properties
| | | |__dbChangelog.xml
| | | |__log4j2.properties
|__pom.xml
Setting up Dependencies
To enable the project to use Spring Data we need the following dependencies
- spring-data-jpa
Modify the pom.xml
file by adding the following dependencies:
<dependencies>
...
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.11.1.RELEASE</version>
</dependency>
</dependencies>
Refactoring the PersonRepository Interface
We would extend the Spring Data Repository
marker interface. The general purpose here is to hold type information as well as being
able to discover interfaces that extend this one during classpath scanning for easy Spring bean creation.
file: src/main/java/com/tutorial/repository/PersonRepository.java
:
public interface PersonRepository extends Repository<Person, Long> {...}
We specify the generic type arguments as the Person
entity and id type (Long
).
Next we will add a Spring Data repository scanning mechanism (@EnableJpaRepositories
) to our configuration class to detect this
interface.
file: src/main/java/com/tutorial/Application.java
:
@Configuration
@EnableJpaRepositories
public class Application {...}
Spring Data JPA requires a bean named entityManagerFactory
; we will modify the LocalContainerEntityManagerFactoryBean
as follows:
file: src/main/java/com/tutorial/Application.java
:
@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManager() {...}
Spring Data uses several query lookup strategies to create queries based on method names.
You can read more on the reference documentation.
To support this so as not to write too much code we will refactor the PersonRepository
.
file: src/main/java/com/tutorial/repository/PersonRepository.java
:
public interface PersonRepository extends Repository<Person, Long> {
// Save or Update
Optional<Person> save(Person person);
Optional<Person> findOne(Long id);
void delete(Person person);
}
Spring Data Repositories are transactional by default that’s why the above snippet is missing the @Transactional
annotation.
Externalizing Configuration
Up until now we have been hardcoding configuration values into our source code. In a big project you would often find yourself
writing configuration values in external files and programmatically calling those values in your source code.
Create a file application.properties
.
file: src/main/resources/application.properties
:
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.javax.persistence.provider=org.hibernate.jpa.HibernatePersistenceProvider
spring.jpa.properties.hibernate.hikari.dataSourceClassName=org.h2.jdbcx.JdbcDataSource
spring.jpa.properties.hibernate.hikari.dataSource.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MVCC=true
spring.jpa.properties.hibernate.hikari.dataSource.user=sa
spring.jpa.properties.hibernate.hikari.dataSource.password=
spring.jpa.properties.hibernate.hikari.minimumIdle=5
spring.jpa.properties.hibernate.hikari.maximumPoolSize=10
spring.jpa.properties.hibernate.hikari.idleTimeout=30000
spring.jpa.properties.hibernate.connection.handling_mode=delayed_acquisition_and_hold
spring.jpa.properties.hibernate.connection.provider_class=org.hibernate.hikaricp.internal.HikariCPConnectionProvider
liquibase.change-log=classpath:/dbChangelog.xml
liquibase.check-change-log-location=true
liquibase.contexts=test
liquibase.drop-first=true
liquibase.enabled=true
The application.properties
file will be loaded into our application using Spring’s Resource
lookup mechanism.
file: src/main/java/com/tutorial/Application.java
:
@Configuration
@EnableJpaRepositories
@PropertySource("classpath:application.properties")
public class Application {
@Inject
private Environment env;
...
}
A Spring Environment
in injected to read property values by key. The full Application
class is provided below.
file: src/main/java/com/tutorial/Application.java
:
@Configuration
@EnableJpaRepositories
@EnableTransactionManagement
@PropertySource("classpath:application.properties")
public class Application {
@Inject
private Environment env;
@Inject
private ResourceLoader resourceLoader;
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(Application.class);
PersonRepository repository = ctx.getBean(PersonRepository.class);
Person person = new Person();
person.setFirstName("Julius");
person.setLastName("Krah");
person.setCreatedDate(LocalDateTime.now());
person.setDateOfBirth(LocalDate.of(1990, Month.APRIL, 4));
// Create person
repository.save(person);
// Hibernate generates id of 1
Optional<Person> p = repository.findOne(1L);
p.ifPresent(consumer -> {
consumer.setModifiedDate(LocalDateTime.now());
consumer.setFirstName("Abeiku");
});
// Update person record
repository.save(p.get());
p = Optional.empty();
// Read updated record
p = repository.findOne(1L);
p.ifPresent(consumer -> {
System.out.format("Person from database: %s", consumer);
});
// Delete person
repository.delete(p.get());
p = Optional.empty();
p = repository.findOne(1L);
((AnnotationConfigApplicationContext) ctx).close();
}
private Properties properties() {
Properties props = new Properties();
props.setProperty("javax.persistence.provider", env.getRequiredProperty("spring.jpa.properties.javax.persistence.provider"));
props.setProperty("javax.persistence.schema-generation.database.action", env.getRequiredProperty("spring.jpa.hibernate.ddl-auto"));
props.setProperty("hibernate.hikari.dataSourceClassName", env.getRequiredProperty("spring.jpa.properties.hibernate.hikari.dataSourceClassName"));
props.setProperty("hibernate.hikari.dataSource.url", env.getRequiredProperty("spring.jpa.properties.hibernate.hikari.dataSource.url"));
props.setProperty("hibernate.hikari.dataSource.user", env.getRequiredProperty("spring.jpa.properties.hibernate.hikari.dataSource.user"));
props.setProperty("hibernate.hikari.dataSource.password", env.getRequiredProperty("spring.jpa.properties.hibernate.hikari.dataSource.password"));
props.setProperty("hibernate.hikari.minimumIdle", env.getRequiredProperty("spring.jpa.properties.hibernate.hikari.minimumIdle"));
props.setProperty("hibernate.hikari.maximumPoolSize", env.getRequiredProperty("spring.jpa.properties.hibernate.hikari.maximumPoolSize"));
props.setProperty("hibernate.hikari.idleTimeout", env.getRequiredProperty("spring.jpa.properties.hibernate.hikari.idleTimeout"));
props.setProperty("hibernate.connection.handling_mode", env.getRequiredProperty("spring.jpa.properties.hibernate.connection.handling_mode"));
props.setProperty("hibernate.connection.provider_class", env.getRequiredProperty("spring.jpa.properties.hibernate.connection.provider_class"));
return props;
}
@Bean(destroyMethod = "close")
public DataSource dataSource() {
HikariConfig config = HikariConfigurationUtil.loadConfiguration(properties());
return new HikariDataSource(config);
}
@Bean
public SpringLiquibase liquibase(DataSource dataSource) {
SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setDataSource(dataSource);
liquibase.setChangeLog(env.getRequiredProperty("liquibase.change-log"));
liquibase.setContexts(env.getRequiredProperty("liquibase.contexts"));
liquibase.setDropFirst(env.getRequiredProperty("liquibase.drop-first", Boolean.class));
liquibase.setShouldRun(env.getRequiredProperty("liquibase.enabled", Boolean.class));
return liquibase;
}
@PostConstruct
public void checkChangelogExists() {
if (env.getRequiredProperty("liquibase.check-change-log-location", Boolean.class)) {
Resource resource = this.resourceLoader.getResource(env.getRequiredProperty("liquibase.change-log"));
Assert.state(resource.exists(), "Cannot find changelog location: " + resource
+ " (please add changelog or check your Liquibase configuration)");
}
}
@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManager() {
LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
entityManager.setPackagesToScan("com.tutorial.entity");
entityManager.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
entityManager.setJpaProperties(properties());
entityManager.setPersistenceUnitName("com.juliuskrah.tutorial");
return entityManager;
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
}
Conclusion
In this post we added Spring Data JPA to reduce CRUD boilerplate code. We also extracted configuration values into an external file.
As usual you can find the full example to this guide in the github repository. Until the next post, keep doing cool things .