Spring Data JPA: An Introduction

Etimbuk U
7 min readJul 9, 2023

--

Have you ever wondered what JPA means? Or have you thought about how it relates to Spring Data JPA? Or Hibernate? Also, what Spring Data JPA is not? And what Hibernate can’t do for you?

In this post, we will discuss these with code examples.

Understanding JPA vs Hibernate vs Spring Data JPA

Before we dive in, it's worth understanding what these terminologies mean.

JPA

JPA until 2019 meant Java Persistence API. JPA currently stands for Jarkarta Persistence API.

So what does JPA do?

JPA’s essential purpose is persistence, a process in which a Java object (for example Customer) outlives the process that created it. This is by storing this object in data storage, think SQL or no-SQL.

Worth noting that on its own JPA is not a framework. It specifies how objects (like Customer) are mapped for persistence.

How does JPA do this?

JPA helps with persistence by specifying several annotations (metadata).

A few JPA annotations

  • Id This specifies the primary for a table within the modeled entity class. For a field or variable to be annotated with Id, it has to be one of the following Java types: String, java.util.Date, java.sql.Date, java.math.BigDecimal, java.math.BigInteger.
  • GeneratedValue This is used in conjunction with Id and specifies generation strategies for the values of primary keys.
  • Column This annotation defines the mapped column for a persistent field. The field or property name is used as the column's name in the table when theColumnannotation is not provided.
  • Entity Specifies that the class is an entity. This annotation is applied to the entity class.
  • Table Specifies the primary table for the annotated entity
  • OneToMany Specifies a many-valued association with one-to-many multiplicity.
  • ManyToOne Specifies a single-valued association to another entity class that has many-to-one multiplicity
  • JoinColumn Specifies a column for joining an entity association or element collection.
@Entity
@Table(name="Customers", schema="dbo")
public class Customer { ... }

JPA & Hibernate

Object Relational Mapping is the automated (and transparent) persistence of objects in a Java application to the tables in an SQL database, using metadata that describes the mapping between the classes of the application and the schema of the SQL database.

From: Java Persistence with Hibernate. 2nd Edition.

Hibernate Object Relational Model (ORM), is an implementation of JPA thereby utilizing JPA standards. A few differences between Hibernate and JPA include

  • Hibernate-related classes are included in org.hibernate package while JPA is in jakarta.persistent package;
  • JPA uses an SQL-like language called Java Persistence Query Language while Hibernate provides Hibernate Query Language
  • JPA provides EntityManager to provide operations to and from the database while Hibernate provides SessionFactory

Spring Data JPA

Spring Data JPA, part of the larger Spring Data family, makes it easy to easily implement JPA based repositories (from Spring Data Docs)

Why Spring Data JPA?

  • As we would see later, with Spring Data JPA, we do not need either the persistence.xml or hibernate.cfg.xml configuration files
  • It understands the JPA mappings (remember Column, Entity, Tableused in our model classes such as Customer above
  • It exposes interfaces such as JpaRepository, CrudRespository, PagingAndSortingRepository. These interfaces help us easily build JPA-specific Create-Read-Update-Delete pagination methods.
  • As we will see below, it removes the need to specifically have call commit methods. This is as we can extend one of the above-mentioned repository interfaces and has methods such as saveAll, save, findById, findAll amongst other methods
  • Underneath the hood to uses hibernate

Next, we will look at how to use JPA, Hibernate, and Spring Data JPA with examples.

The How ⚙️

Before we proceed to how to use all of JPA, Hibernate, and Spring Data JPA, let's first look at a few vanilla examples. This will include

  • Vanilla JPA to persist and retrieve data;
  • Vanilla Hibernate to achieve the same persisting retrieval of data; and
  • Spring Data JPA

Before we move into our code examples, it is worth setting up a database and a Java project, and for this post, we will be using PostgreSQL on Docker.

Setting up Postgres on Docker

Setting up Postgresql with Docker

Just JPA

Why start with a vanilla JPA example you might ask? This is because it is worth gaining a from-the-scratch understanding of how JPA fits in Hibernate and how both fit into Spring Data JPA.

For our vanilla JPA example, all we need to do include

  • adding the below dependencies to our project
// To build.gradle
// Properties
def versions = [
'hibernateCore' : '6.2.4.Final',
'postgres' : '42.6.0',
]

// Postgres
implementation group: 'org.postgresql', name: 'postgresql', version: versions.postgres

// Hibernate
implementation group: 'org.hibernate.orm', name: 'hibernate-core', version: versions.hibernateCore

Whoops!! Did we add hibernate-core dependency to our vanilla JPA project?

Yes, we did, this is because it brings in the JPA’s jakarta.persistence dependency.

Dependencies pulled in as part of hibernate-core
  • Create a Customer class using JPA to specify how we want to map it to thecustomers database table
@Entity(name = "customers")
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;

@Column(name = "username")
private String username;

@Column(name = "fullName")
private String fullName;

@Column(name = "joinedOn")
private LocalDate joinedOn;
}
  • Next, we create a persistence.xml file in resources > META-INF folder of your project. persistence.xml, configures at least one persistent unit and each unit must have a unique name. Think of a persistence unit as a configuration for — database details to be used, classes to be mapped to said database tables, how and if you want database and/or tables to be recreated, and how to log SQL amongst others
<persistence-unit name="vanilla-jpa-unit">
<description>A demo persisting to a DB with vanilla JPA</description>

<class>dev.etimbuk.models.Customer</class>

<properties>
<!-- Database connection details -->
<property name="jakarta.persistence.jdbc.driver" value="org.postgresql.Driver" />
<property name="jakarta.persistence.jdbc.url" value="jdbc:postgresql://localhost:5432/blogposts" />
<property name="jakarta.persistence.jdbc.user" value="yourusername" />
<property name="jakarta.persistence.jdbc.password" value="yourpassword" />

<!-- DON'T USE THIS IN PRODUCTION -->
<!-- automatically drop and create required database tables -->
<property name="jakarta.persistence.schema-generation.database.action" value="drop-and-create" />
</properties>

</persistence-unit>
  • Now that we have our hibernate-core dependency, mapped class and persistence.xml in place, let’s test that we can save to our customers table and retrieve data from it.
// First we will create an EntityManager from jakarta.persistence package
final EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("vanilla-jpa-unit");
final EntityManager entityManager = entityManagerFactory.createEntityManager();

//Create a basic Customer instance
final var customer = new Customer("Test.User", "Test User", LocalDate.now().minusYears(5));

//Persist data into customers table
entityManager.getTransaction().begin();
entityManager.persist(customer);
entityManager.getTransaction().commit();

//SELECT query to verify above persist works as expected
List<Customer> savedCustomers = entityManager.createQuery("SELECT c FROM customers c", Customer.class).getResultList();

//assert we only have one customer in customers table
assertEquals(1, savedCustomers.size());

Complete code sample for Just JPA section available here

JPA + Hibernate

We can use JPA and Hibernate in two ways

  • using the hibernate.cfg.xml to configure database details as we did above via persistence.xml
  • applying configuration through StandardServiceRegistryBuilder class.

Worth noting here that

  • same Customer class as above is still used;
  • EntityFactory switched for SessionFactory hibernate class;
  • same hibernate-core dependency is still used.
//Create SessionFactory instance
sessionFactory = metaDataSources.buildMetadata().buildSessionFactory();

//Create a basic Customer instance
final var customer = new Customer("Test.User", "Test User", LocalDate.now().minusYears(5));

//Create a Session. Think of this as a JDBC connection
try (final Session session = sessionFactory.openSession()){
session.beginTransaction();
session.persist(customer);
session.getTransaction().commit();

//SELECT query to verify above persist works as expected
List<Customer> savedCustomers = session.createQuery("SELECT c FROM customers c", Customer.class).getResultList();

//assert we only have one customer in customers table
assertEquals(1, savedCustomers.size());
}

Complete code sample for JPA + Hibernate section available here

JPA, Hibernate & Spring Data JPA

Now, we have looked at how we can use JPA on its own and JPA + Hibernate together, next, we will create a basic CRUD application to understand why it would be preferred to use Spring Data JPA.

Steps

  • Let’s update our build.gradle file to include all needed dependencies
dependencies {
compileOnly group: 'org.projectlombok', name: 'lombok', version: versions.lombok
annotationProcessor group: 'org.projectlombok', name: 'lombok', version: versions.lombok

// This dependency is used by the application.
implementation group: 'com.google.guava', name: 'guava', version: versions.googleGuava

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'

// Logging
implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: versions.log4j
implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: versions.log4j

// Postgres
runtimeOnly group: 'org.postgresql', name: 'postgresql', version: versions.postgres

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testCompileOnly group: 'org.projectlombok', name: 'lombok', version: versions.lombok
testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version: versions.lombok
}
  • Next, we create an application.properties file.
# jdbc.config
spring.datasource.url=jdbc:postgresql://localhost:5432/blogposts
spring.datasource.username=yourusername
spring.datasource.password=yourpassword
spring.datasource.driver-class-name=org.postgresql.Driver

#DON'T DO THIS IN PRODUCTION
spring.jpa.hibernate.ddl-auto=create-drop
  • Given we have also included Spring Boot (yup!!! 😃), we also need to create our entry point to the application
@SpringBootApplication
public class JPAHibernateSpringDataJPAApplication {
public static void main(String[] args) {
SpringApplication.run(JPAHibernateSpringDataJPAApplication.class, args);
}
}
  • Now, we extend JpaRepository to create our CustomerRepository. Think of this as our data access class
  • Next, we create a service class that uses our CustomerRepository class with basic CRUD methods
 public Optional<Customer> findById(final String customerId) {
return customerRepo.findById(customerId);
}

public Customer create(final Customer customer) {
return customerRepo.save(customer);
}

// ...
  • Now, some tests
@BeforeEach
void init() {
expectedCustomerToSave = new Customer("firstname", "surname", LocalDate.now());
savedCustomer = service.create(expectedCustomerToSave);
}

@Test
void testExistingCustomerUpdated() {
final var newDate = LocalDate.now().plusDays(100);
final var existingCustomer = service.findById(savedCustomer.getId()).get();
existingCustomer.setJoinedOn(newDate);

final var updatedCustomer = service.update(existingCustomer);
assertEquals(newDate.toString(), updatedCustomer.get().getJoinedOn().toString());
}

Complete code sample for JPA, Hibernate & Spring Data JPA section available here

In Summary

Wheew!!!!

In this post, we have looked at what JPA, Hibernate, and Spring Data JPA are and illustrated with code examples how we can use them independently to map and persist data in a database.

As is the practice all the code examples in this post are available here

--

--

Etimbuk U

Java | Flutter | Android | Some JavaScript | API Design