1. Overview
We have multiple options for connecting to a database using Java applications. Usually, we refer to different layers, starting from JDBC. Then, we move to JPA, with implementations like Hibernate. JPA will eventually use JDBC but make it more transparent to the user with an Object-Entity management approach.
Finally, we can have a framework-like integration, for example, Spring Data JPA, with pre-defined interfaces to access entities but still using JPA and an entity manager under the hood.
In this tutorial, we’ll talk about the difference between Spring Data JPA and JPA. We’ll also explain how they both work with some high-level overviews and code snippets. Let’s start by explaining some of the history of JDBC and how JPA came to be.
2. From JDBC to JPA
Since 1997 version 1.1 of JDK (Java Development Kit), we’ve had access to relational databases with JDBC.
The key points about JDBC that are also essential to understand JPA include:
- DriverManager and interfaces to connect and execute queries: This enables connections to any ODBC-accessible data source commonly using a specific driver, for example, a MySQL Java connector. We can connect to the database and open/close a transaction over it. Most importantly, we can use any database like MySQL, Oracle, or PostgreSQL only by changing the database driver.
- Data Source: For both Java Enterprise and frameworks like Spring, this is important to understand how we can define and get a database connection in the working context.
- Connection pool, which acts like a cache of database connection objects: We can reuse open connections that live in active/passive states and reduce the number of times they are created.
- Distributed transactions: These consist of one or more statements that update data on multiple databases or resources within the same transaction.
After JDBC’s creation, persistence frameworks (or ORM tools) like Hibernate, which maps database resources as plain old Java objects, started to appear. We refer to ORM as the layer that defines, for example, the schema generation or the database dialect.
Also, Entity Java Bean (EJB) creates standards to manage server-side components encapsulating the business logic of an application. Features like transactional processing, JNDI, and persistence services are now Java beans. Furthermore, Annotation and Dependency Injection now simplify the configuration and integration of different systems.
With the EJB 3.0 release, persistence frameworks were incorporated into the Java Persistence API (JPA), and projects such as Hibernate or EclipseLink have become implementations of the JPA specification.
3. JPA
With JPA, we can write building blocks in an object-oriented syntax independent of the database we are using.
To demonstrate, let’s see an example of an employee table definition. We can finally define a table as a POJO using the @Entity annotation:
@Entity
@Table(name = "employee")
public class Employee implements Serializable {
@Id
@Generated
private Long id;
@Column(nullable = false)
private String firstName;
// other fields, setter and getters
}
JPA classes can manage database table features, such as primary key strategies and relationships like many-to-many. This is relevant, for instance, while using foreign keys. JPA can do lazy initialization of collections and get access to the data only when we need to.
We can perform all the CRUD operations (create, retrieve, update, delete) on entities using EntityManager. JPA is implicitly handling transactions. This can be done through a container like Spring transaction management, or simply by the ORM tools like Hibernate using EntityManager.
Once we access the EntityManager, we can, for example, persist an Employee:
Employee employee = new Employee();
// set properties
entityManager.persist(employee);
3.1. Criteria Queries and JPQL
We can then, for example, find an Employee by id:
Employee response = entityManger.find(Employee.class, id);
More interestingly, we can use Criteria Queries in a type-safe way to interact with an @Entity. For example, still finding by id, we can use the CriteriaQuery interface:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Employee> cr = cb.createQuery(Employee.class);
Root<Employee> root = cr.from(Employee.class);
cr.select(root);
criteriaQuery.where(criteriaBuilder.equal(root.get(Employee_.ID), employee.getId()));
Employee employee = entityManager.createQuery(criteriaQuery).getSingleResult();
Furthermore, we can also apply sorting and pagination:
criteriaQuery.orderBy(criteriaBuilder.asc(root.get(Employee_.FIRST_NAME)));
TypedQuery<Employee> query = entityManager.createQuery(criteriaQuery);
query.setFirstResult(0);
query.setMaxResults(3);
List<Employee> employeeList = query.getResultList();
We can use criteria queries for persistence. For example, we can do an update using the CriteriaUpdate interface. Suppose we want to update the email address of an employee:
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaUpdate<Employee> criteriaQuery = criteriaBuilder.createCriteriaUpdate(Employee.class);
Root<Employee> root = criteriaQuery.from(Employee.class);
criteriaQuery.set(Employee_.EMAIL, email);
criteriaQuery.where(criteriaBuilder.equal(root.get(Employee_.ID), employee));
entityManager.createQuery(criteriaQuery).executeUpdate();
Finally, JPA also provides JPQL (or HQL if we natively use Hibernate), which allows us to create a query in an SQL-like syntax, but still referring to the @Entity bean:
public Employee getEmployeeById(Long id) {
Query jpqlQuery = getEntityManager().createQuery("SELECT e from Employee e WHERE e.id=:id");
jpqlQuery.setParameter("id", id);
return jpqlQuery.getSingleResult();
}
3.2. JDBC
JPA can adapt to many different databases with generic interfaces. However, in real-life applications, most likely, we will need JDBC support. This is to use specific database query syntax or for performance reasons like, for example, in batch processing.
Even if we use JPA, we can still write in a database’s native language using the createNativeQuery method. For example, we might want to use the rownum Oracle keyword:
Query query = entityManager
.createNativeQuery("select * from employee where rownum < :limit", Employee.class);
query.setParameter("limit", limit);
List<Employee> employeeList = query.getResultList();
Furthermore, this works for all sorts of functions and procedures that are still related to a database-specific language. For example, we can create and execute a stored procedure:
StoredProcedureQuery storedProcedure = em.createStoredProcedureQuery("calculate_something");
// set parameters
storedProcedure.execute();
Double result = (Double) storedProcedure.getOutputParameterValue("output");
3.3. Annotations
JPA comes with a set of annotations. We’ve already seen @Table, @Entity, @Id, and @Column.
If we often reuse a query, we can annotate it as @NamedQuery at the class level with @Entity, still using JPQL:
@NamedQuery(name="Employee.findById", query="SELECT e FROM Employee e WHERE e.id = :id")
Then, we can create a Query from the template:
Query query = em.createNamedQuery("Employee.findById", Employee.class);
query.setParameter("id", id);
Employee result = query.getResultList();
Similarly to @NamedQuery, we can use @NamedNativeQuery for a database native query:
@NamedNativeQuery(name="Employee.findAllWithLimit", query="SELECT * FROM employee WHERE rownum < :limit")
3.4. Metamodel
We might want to generate a metamodel that allows us to statically access table fields in a type-safe way. For example, let’s see the Employee_ class that generates from Employee:
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Employee.class)
public abstract class Employee_ {
public static volatile SingularAttribute<Employee, String> firstName;
public static volatile SingularAttribute<Employee, String> lastName;
public static volatile SingularAttribute<Employee, Long> id;
public static volatile SingularAttribute<Employee, String> email;
public static final String FIRST_NAME = "firstName";
public static final String LAST_NAME = "lastName";
public static final String ID = "id";
public static final String EMAIL = "email";
}
We can statically access these fields. The class will regenerate the class if we make changes to the data model.
4. Spring Data JPA
Part of the large Spring Data family, Spring Data JPA is built as an abstraction layer over the JPA. So, we have all the features of JPA plus the Spring ease of development.
For years, developers have written boilerplate code to create a JPA DAO for basic functionalities. Spring helps to significantly reduce this amount of code by providing minimal interfaces and actual implementations.
4.1. Repositories
For example, suppose we want to create a CRUD repository for the Employee table. We can use the JpaRepository:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
That’s all we need for a start. So, if we want to persist or update, we can get an instance of the repository and save an employee:
employeeRepository.save(employee);
We have great support also for writing queries. Interestingly, we can define query methods by simply declaring their method signatures:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
List<Employee> findByFirstName(String firstName);
}
Spring will create repository implementations automatically, at runtime, from the repository interface.
So, we can use these methods without having to implement them:
List<Employee> employees = employeeRepository.findByFirstName("John");
We have also support for sorting and paginating repositories:
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
We can then create a Pageable object with the page size, number, and sorting criteria:
Pageable pageable = PageRequest.of(5, 10, Sort.by("firstName"));
Page<Employee> employees = employeeRepositorySortAndPaging.findAll(pageable);
4.2. Queries
Another great feature is the extensive support for the @Query annotation. Similarly to JPA, this helps to define JPQL-like or native queries. Let’s see an example of how we can use it in the repository interface to get a list of employees by applying a sort:
@Query(value = "SELECT e FROM Employee e")
List<Employee> findAllEmployee(Sort sort);
Again, we’ll use the repository and fetch the list:
List<Employee> employees = employeeRepository.findAllEmployee(Sort.by("firstName"));
4.3. QueryDsl
Likewise the JPA, we have criteria-like support called QueryDsl that also has a metamodel generation. For example, suppose we want a list of employees, filtering on the name:
QEmployee employee = QEmployee.employee;
List<Employee> employees = queryFactory.selectFrom(employee)
.where(
employee.firstName.eq("John"),
employee.lastName.eq("Doe"))
.fetch();
5. JPA Tests
Let’s create and test a simple JPA application. We can make Hibernate manage the transactionality.
5.1. Dependencies
Let’s have a look at the dependencies. We’ll need JPA, Hibernate core, and H2 database imports in our pom.xml.
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.4.2.Final</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
</dependency>
Furthermore, we need a plugin for the metamodel generation:
<plugin>
<groupId>org.bsc.maven</groupId>
<artifactId>maven-processor-plugin</artifactId>
<version>3.3.3</version>
<executions>
<execution>
<id>process</id>
<goals>
<goal>process</goal>
</goals>
<phase>generate-sources</phase>
<configuration>
<outputDirectory>${project.build.directory}/generated-sources</outputDirectory>
<processors>
<processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
</processors>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>6.4.2.Final</version>
</dependency>
</dependencies>
</plugin>
5.2. Configuration
To keep it plain JPA, we use a persistence.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
<persistence-unit name="pu-test">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>com.baeldung.spring.data.persistence.springdata_jpa_difference.model.Employee</class>
<properties>
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
</properties>
</persistence-unit>
</persistence>
We don’t need any Bean-based configuration.
5.3. Test Class Definition
To demonstrate, let’s create a test class. We’ll get the EntityManager with the createEntityManagerFactory method and manage transactions manually:
public class JpaDaoIntegrationTest {
private final EntityManagerFactory emf = Persistence.createEntityManagerFactory("pu-test");
private final EntityManager entityManager = emf.createEntityManager();
@Before
public void setup() {
deleteAllEmployees();
}
// tests
private void deleteAllEmployees() {
entityManager.getTransaction()
.begin();
entityManager.createNativeQuery("DELETE from Employee")
.executeUpdate();
entityManager.getTransaction()
.commit();
}
public void save(Employee entity) {
entityManager.getTransaction()
.begin();
entityManager.persist(entity);
entityManager.getTransaction()
.commit();
}
public void update(Employee entity) {
entityManager.getTransaction()
.begin();
entityManager.merge(entity);
entityManager.getTransaction()
.commit();
}
public void delete(Long employee) {
entityManager.getTransaction()
.begin();
entityManager.remove(entityManager.find(Employee.class, employee));
entityManager.getTransaction()
.commit();
}
public int update(CriteriaUpdate<Employee> criteriaUpdate) {
entityManager.getTransaction()
.begin();
int result = entityManager.createQuery(criteriaUpdate)
.executeUpdate();
entityManager.getTransaction()
.commit();
entityManager.clear();
return result;
}
}
5.4. Testing JPA
First, we want to test if we can find an employee by id:
@Test
public void givenPersistedEmployee_whenFindById_thenEmployeeIsFound() {
// save employee
assertEquals(employee, entityManager.find(Employee.class, employee.getId()));
}
Let’s see other ways we can find an Employee. For example, we can use the CriteriaQuey:
@Test
public void givenPersistedEmployee_whenFindByIdCriteriaQuery_thenEmployeeIsFound() {
// save employee
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Employee> criteriaQuery = criteriaBuilder.createQuery(Employee.class);
Root<Employee> root = criteriaQuery.from(Employee.class);
criteriaQuery.select(root);
criteriaQuery.where(criteriaBuilder.equal(root.get(Employee_.ID), employee.getId()));
assertEquals(employee, entityManager.createQuery(criteriaQuery)
.getSingleResult());
}
Also, we can use JPQL:
@Test
public void givenPersistedEmployee_whenFindByIdJpql_thenEmployeeIsFound() {
// save employee
Query jpqlQuery = entityManager.createQuery("SELECT e from Employee e WHERE e.id=:id");
jpqlQuery.setParameter("id", employee.getId());
assertEquals(employee, jpqlQuery.getSingleResult());
}
Let’s see how we can create a Query from @NamedQuery:
@Test
public void givenPersistedEmployee_whenFindByIdNamedQuery_thenEmployeeIsFound() {
// save employee
Query query = entityManager.createNamedQuery("Employee.findById");
query.setParameter(Employee_.ID, employee.getId());
assertEquals(employee, query.getSingleResult());
}
Let’s also see an example of how to apply sorting and pagination:
@Test
public void givenPersistedEmployee_whenFindWithPaginationAndSort_thenEmployeesAreFound() {
// save John, Frank, Bob, James
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Employee> criteriaQuery = criteriaBuilder.createQuery(Employee.class);
Root<Employee> root = criteriaQuery.from(Employee.class);
criteriaQuery.select(root);
criteriaQuery.orderBy(criteriaBuilder.asc(root.get(Employee_.FIRST_NAME)));
TypedQuery<Employee> query = entityManager.createQuery(criteriaQuery);
query.setFirstResult(0);
query.setMaxResults(3);
List<Employee> employeeList = query.getResultList();
assertEquals(Arrays.asList(bob, frank, james), employeeList);
}
Finally, let’s see how to update an employee email using the CriteriaUpdate:
@Test
public void givenPersistedEmployee_whenUpdateEmployeeEmailWithCriteria_thenEmployeeHasUpdatedEmail() {
// save employee
String updatedEmail = "[email protected]";
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaUpdate<Employee> criteriaUpdate = criteriaBuilder.createCriteriaUpdate(Employee.class);
Root<Employee> root = criteriaUpdate.from(Employee.class);
criteriaUpdate.set(Employee_.EMAIL, updatedEmail);
criteriaUpdate.where(criteriaBuilder.equal(root.get(Employee_.ID), employee.getId()));
assertEquals(1, update(criteriaUpdate));
assertEquals(updatedEmail, entityManager.find(Employee.class, employee.getId())
.getEmail());
}
6. Spring Data JPA Tests
Let’s see how we can improve by adding Spring repositories and in-built query support.
6.1. Dependencies
In this case, we need to add the Spring Data dependency. We also need the QueryDsl dependency for the fluent query API.
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>3.1.5</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.214</version>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
<classifier>jakarta</classifier>
</dependency>
6.2. Configuration
First, let’s create our configuration:
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackageClasses = EmployeeRepository.class)
public class SpringDataJpaConfig {
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan(Employee.class.getPackage().getName());
JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
Properties properties = new Properties();
properties.setProperty("hibernate.hbm2ddl.auto", "create-drop");
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
em.setJpaProperties(properties);
return em;
}
@Bean
public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactoryBean.getObject());
return transactionManager;
}
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1")
.driverClassName("org.h2.Driver")
.username("sa")
.password("sa")
.build();
}
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
return new JPAQueryFactory((entityManager));
}
}
Finally, let’s have a look at our JpaRepository:
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
List<Employee> findByFirstName(String firstName);
@Query(value = "SELECT e FROM Employee e")
List<Employee> findAllEmployee(Sort sort);
}
Also, we want to use a PagingAndSortingRepository:
@Repository
public interface EmployeeRepositoryPagingAndSort extends PagingAndSortingRepository<Employee, Long> {
}
6.3. Test Class Definition
Let’s see our test class for Spring Data tests. We’ll rollback to keep every test atomic:
@ContextConfiguration(classes = SpringDataJpaConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
@Transactional
@Rollback
public class SpringDataJpaIntegrationTest {
@Autowired
private EmployeeRepository employeeRepository;
@Autowired
private EmployeeRepositoryPagingAndSort employeeRepositoryPagingAndSort;
@Autowired
private JPAQueryFactory jpaQueryFactory;
// tests
}
6.4. Testing Spring Data JPA
Let’s start with finding an employee by id:
@Test
public void givenPersistedEmployee_whenFindById_thenEmployeeIsFound() {
Employee employee = employee("John", "Doe");
employeeRepository.save(employee);
assertEquals(Optional.of(employee), employeeRepository.findById(employee.getId()));
}
Let’s see how to find employees by their first names:
@Test
public void givenPersistedEmployee_whenFindByFirstName_thenEmployeeIsFound() {
Employee employee = employee("John", "Doe");
employeeRepository.save(employee);
assertEquals(employee, employeeRepository.findByFirstName(employee.getFirstName())
.get(0));
}
We can apply sorting, for example, when querying all employees:
@Test
public void givenPersistedEmployees_whenFindSortedByFirstName_thenEmployeeAreFoundInOrder() {
Employee john = employee("John", "Doe");
Employee bob = employee("Bob", "Smith");
Employee frank = employee("Frank", "Brown");
employeeRepository.saveAll(Arrays.asList(john, bob, frank));
List<Employee> employees = employeeRepository.findAllEmployee(Sort.by("firstName"));
assertEquals(3, employees.size());
assertEquals(bob, employees.get(0));
assertEquals(frank, employees.get(1));
assertEquals(john, employees.get(2));
}
Let’s have a look at how to build a query with QueryDsl:
@Test
public void givenPersistedEmployee_whenFindByQueryDsl_thenEmployeeIsFound() {
Employee john = employee("John", "Doe");
Employee frank = employee("Frank", "Doe");
employeeRepository.saveAll(Arrays.asList(john, frank));
QEmployee employeePath = QEmployee.employee;
List<Employee> employees = jpaQueryFactory.selectFrom(employeePath)
.where(employeePath.firstName.eq("John"), employeePath.lastName.eq("Doe"))
.fetch();
assertEquals(1, employees.size());
assertEquals(john, employees.get(0));
}
Finally, we can check how to use the PagingAndSortingRepository:
@Test
public void givenPersistedEmployee_whenFindBySortAndPagingRepository_thenEmployeeAreFound() {
Employee john = employee("John", "Doe");
Employee bob = employee("Bob", "Smith");
Employee frank = employee("Frank", "Brown");
Employee jimmy = employee("Jimmy", "Armstrong");
employeeRepositoryPagingAndSort.saveAll(Arrays.asList(john, bob, frank, jimmy));
Pageable pageable = PageRequest.of(0, 2, Sort.by("firstName"));
Page<Employee> employees = employeeRepositoryPagingAndSort.findAll(pageable);
assertEquals(Arrays.asList(bob, frank), employees.get()
.collect(Collectors.toList()));
}
7. How JPA and Spring Data JPA Differ
JPA defines the standard approach for object-relational mapping (ORM).
It provides an abstraction layer that makes it independent from the database we are using. JPA can also handle transactionality and is built over JDBC, so we can still use the native database language.
Spring Data JPA is yet another layer of abstraction over the JPA. However, it is more flexible than JPA and offers simple repositories and syntax for all CRUD operations. We can remove all the boilerplate code from our JPA applications and use simpler interfaces and annotations. Furthermore, we will have Spring’s ease of development, for example, to handle transactionality transparently.
Besides, there wouldn’t be Spring Data JPA without JPA, so in any case, JPA is a good starting point if we want to learn more about the Java database access layer.
8. Conclusion
In this tutorial, we have shortly seen a JDBC history and why JPA has become a standard for relational database API. We also saw examples of JPA and Spring Data JPA for persisting entities and creating dynamic queries. Finally, we presented some test cases to show the difference between a vanilla JPA application and a Spring Data JPA application.
As always, all source code is available over on GitHub.