eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

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>jakarta.persistence</groupId>
    <artifactId>jakarta.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="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"/>
            <property name="jakarta.persistence.jdbc.user" value="sa"/>
            <property name="jakarta.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.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – LSD – NPI (cat=JPA)
announcement - icon

Get started with Spring Data JPA through the reference Learn Spring Data JPA:

>> CHECK OUT THE COURSE

eBook Jackson – NPI EA – 3 (cat = Jackson)