Spring Data JPA. 2. Sample Project

logo spring

In this chapter we’ll create the sample project for the course from scratch. Since the course focuses on Spring Data JPA, I’ll ensure the project doesn’t require a deep understanding of the Spring Framework and JPA. The simpler, the better. Simplicity is beautiful.


>> Read this post in Spanish here <<

>> List of Chapters <<

Contents

  1. Introduction
  2. Note About Versions (JEE vs. Jakarta EE)
  3. Get the Project
  4. Project Description
    1. Pom file
    2. Lombok
    3. The @SpringBootApplication Class
    4. Database
      1. HyperSQL
      2. Connection Configuration
      3. Tables and Records
    5. JPA Entity Classes
    6. Automatically Generated Classes
    7. Spring Data, JPA, and Hibernate Integration
    8. Logging with Slf4j and Logback
    9. Testing with Spring Boot, JUnit 5, and AssertJ
  5. Running the Project
    1. Development Environments
    2. Maven Wrapper
  6. Summary
  7. Source Code


Introduction

Spring Boot is the most convenient way to code with the vast development ecosystem that Spring Framework delivers. It makes it easy to manage dependencies and configurations, and it’s also a cornucopia of handy tools. That’s why the sample project is a Maven project based on Spring Boot 3.2.2 (January 2024).

Note About Versions (JEE vs. Jakarta EE)

Spring Boot 3, Spring Data 3, and Spring 6 migrated from Java EE specifications (JEE) to Jakarta EE specifications. The latter is the evolution of the former, so JEE 8 is followed by Jakarta EE 9, Jakarta EE 9.1, etc. Put simply, JEE became Jakarta EE. Both include JPA.

The most important change that brought the conversion of JEE into Jakarta EE was the renaming of the packages from javax to jakarta. For example, JPA classes, annotations, and interfaces were moved from javax.persistence (JEE) to jakarta.persistence (Jakarta EE). Be aware of this package refactoring when coding with earlier versions of Spring and when consulting old books and tutorials.

Why and how did JEE become Jakarta EE? Wikipedia offers a short explanation.

Get the Project

The project is available as a git repository on GitHub. If you don’t know how to download it and import it into your development environment (IDE), check out this post:

Project Description

Pom file

As we have a Maven project, let’s look at its pom file. Thanks to Spring Boot, mile-long poms are a thing of the past.

To use Spring Boot, we must set up the following parent pom:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.2</version>
    <relativePath/>
</parent>

The artifact adds the configuration for some Maven plugins and other details, such as the Java version. Spring Boot 3 requires Java 17. I usually use the jdk’s published by Eclipse Adoptium. For Linux and macOS I recommend Sdkman, a utility that manages the installation of Java runtimes and other development tools.

A Spring Boot starter is a set of convenient dependency descriptors that brings together the dependencies that constitute a specific Spring module or functional set. To name a few: web development and RESTful with Spring MVC, security, management and monitoring with Spring Actuator, access to databases via JDBC…

Besides the essential spring-boot-starter-parent, our project requires two starters:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
 </dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

spring-boot-starter-data-jpa is the Spring Data JPA starter. It includes Spring Data Commons, Spring Data JPA, and Hibernate.

spring-boot-starter-test includes classes, annotations, and libraries to develop tests that need to start Spring and its beans. Beans are the objects managed by Spring’s dependency injection system. Understanding this is essential; again, I refer you to this tutorial.

Believe it or not, we get everything we want from Spring with just a couple of dependencies. Do you see something strange in them? Their version number is missing! The Spring Boot parent pom sets it. This technique guarantees compatibility between the libraries that the starters import.

Although we’ll change the pom in future sections and chapters, it already contains the basics.

The table below details the main libraries of the project.

LibraryVersion
Spring Framework (core)6.1.3
Spring Data (Commons y JPA)3.2.2
JPA (Jakarta Persistance)3.1
Hibernate6.4.1
HyperSQL2.7.2
JUnit Jupiter5.10.1
Lombok1.18.30
AssertJ3.24.2

Lombok

I simplify my code with Lombok. This library provides annotations that saves us from writing boilerplate code that clutters our programs, such as accessors and mutators methods (getters and setters), constructors, toString methods, and more. An IDE can create most of that code, but Lombok offers two benefits:

  • Lombok annotations generate the code at compiling time, so it’s never outdated. For instance, if you add a field to a class, you don’t have to remember to ask the IDE to regenerate the method toString().
  • Lombok doesn’t add the generated code to the project’s source files so that it won’t bother our wearied eyes. 

I’ll explain some Lombok annotations as soon as they appear in the course for the first time.

Spring Boot supports Lombok, so we add the dependency to the pom without specifying the version:

 <dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
</dependency>

In addition, you must configure your IDE to cast Lombok’s magic when compiling code:

  • Eclipse. Download this JAR file. It’s an executable application where you specify the location of the Eclipse executable file. If it doesn’t open when you double-click it, try using the java -jar lombok.jar command.
  • IntelliJ. It’s compatible with Lombok since version 2020.3. In earlier versions, install the plugin available in the Marketplace (“File” -> “Settings” -> “Plugins”).

The @SpringBootApplication Class

Every Spring Boot project must have a class marked with @SpringBootApplication and containing a main method:

package com.danielme.springdatajpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootApp {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootApp.class, args);
    }

}

main() runs the project as a Spring application that enjoys the Spring Boot capabilities.

The class SpringBootApp that you’ll find in the git repository contains more annotations. We’ll talk about them in the chapters where we need them.

Database

HyperSQL

The database is a core component of the project to the point that its utilization with Spring Data JPA is the subject of the course. My first choice is usually MySQL, arguably the most popular open-source relational database. But I want you to be able to run the project without installing a database or running a Docker container. As I said before, the simpler the project, the better.

That’s why I’ve opted for HyperSQL (a.k.a HSQLDB), one of the embedded databases supported by Spring Boot and Hibernate. This type of database is packaged together with the application, and when the application starts or stops, the database does the same. I’ll use the in-memory mode so that all the data disappears when HyperSQL stops. Hence the database will be a “blank canvas” when the application starts.

If you use MySQL or any similar database, I suggest utilizing Testcontainers for automated testing.

Let’s add HyperSQL to the pom:

 <dependency>
     <groupId>org.hsqldb</groupId>
     <artifactId>hsqldb</artifactId>
     <scope>runtime</scope>
 </dependency>
Connection Configuration

We define the connection data in the /src/main/resources/application.properties file. Spring Boot centralizes the configuration properties for Spring Framework and other libraries in that file. This page lists the supported properties, and in this post I explain how to add custom properties.

These are the settings for connecting to an in-memory HyperSQL database:

spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver
spring.datasource.url=jdbc:hsqldb:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=

You can skip them as they set the default values—actually, they’re commented out in the project.

Spring Boot uses the above settings to create a bean of the DataSource interface, which belongs to the JDBC API. That bean provides the connections to the database. You’ve seen that bean in action in the first chapter:

@Autowired
private DataSource dataSource;
 
public List<Country> findByPopulationLessThan(int maxPopulation) throws SQLException {
    String sql = "SELECT * FROM countries WHERE population < ? ORDER BY name";
    try (Connection connection = dataSource.getConnection();
         PreparedStatement ps = connection.prepareStatement(sql);) {
         ps.setInt(1, maxPopulation);
         ResultSet resultSet = ps.executeQuery();
         return mapResults(resultSet);
    }
}

Spring Boot relies on the DataSource implementation provided by HikariCP, a high-performance connection pool. In a real project, you should fine-tune the Hikari configuration with the spring.datasource.hikari properties.

Tables and Records

The spring.jpa.hibernate.ddl-auto property sets what Hibernate will do during its startup regarding the coherency of tables and the entity model that I’ll talk about shortly. Here are the values it can take.

During the early stages of a project, it’s common to choose the update option. It instructs Hibernate to synchronize the tables and the entity classes. To achieve this, Hibernate generates and executes the DDL SQL statements it deems appropriate. It will even create the tables when necessary.

Note. If you’re wondering, DDL stands for data definition language. It consists of SQL commands (CREATE, ALTER, DROP…) that build and modify the database’s schema (tables, columns, indexes, constraints, etc.).

But managing the database schema with scripts is more precise and flexible. I’ll do this, so I might set ddl-auto to none to instruct Hibernate to do nothing; it would be unnecessary, for it’s the default value. Instead, I’ll use validate to tell Hibernate to verify whether the entity classes are compatible with the tables.

spring.jpa.hibernate.ddl-auto=validate

If Hibernate finds any inconsistencies, it will throw an exception that will stop the application from starting. No prizes for guessing what’s the problem—the error message is plain as day. An example:

org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing column [population] in table [countries]

How do we create the schema with a script? Let’s take advantage of Spring Boot conventions and place the DDL statements that create the schema in the /src/main/resources/schema.sql file:

CREATE SEQUENCE confederations_seq INCREMENT BY 50;
CREATE TABLE confederations
(
    id   BIGINT GENERATED BY DEFAULT AS SEQUENCE confederations_seq,
    name VARCHAR(255) NOT NULL UNIQUE,
    PRIMARY KEY (id)
);

CREATE SEQUENCE countries_seq INCREMENT BY 50;
CREATE TABLE countries
(
    id                       BIGINT GENERATED BY DEFAULT AS SEQUENCE countries_seq,
    name                     VARCHAR(50) NOT NULL UNIQUE,
    population               INTEGER     NOT NULL,
    oecd                     BOOLEAN     NOT NULL,
    capital                  VARCHAR(50) NOT NULL UNIQUE,
    united_nations_admission DATE,
    confederation_id         BIGINT,
    PRIMARY KEY (id)
);

ALTER TABLE countries ADD FOREIGN KEY (confederation_id) REFERENCES confederations (id);

Note. The script contains a third table, named users, which we’ll only use in the last chapter.

There are two tables. They store data about countries and the soccer confederation they’re members of (if applicable). We’ll examine the columns shortly.

The file /src/test/resources/data.sql contains the SQL code that populates the tables with the dataset that will support the tests we’ll write in the course:

INSERT INTO confederations (id, name)
VALUES (1, 'UEFA');
INSERT INTO confederations (id, name)
VALUES (2, 'CONCACAF');
INSERT INTO confederations (id, name)
VALUES (3, 'CONMEBOL');
INSERT INTO confederations (id, name)
VALUES (4, 'AFC');

INSERT INTO countries (id, name, capital, population, oecd, united_nations_admission, confederation_id)
VALUES (1, 'Norway', 'Oslo', 5136700, true, '1945-11-27', 1),
       (2, 'Spain', 'Madrid', 47265321, true, '1955-12-14', 1),
       (3, 'Mexico', 'Mexico City', 115296767, true, '1945-11-7', 2),
       (4, 'Colombia', 'Bogotá', 47846160, true, '1945-11-5', 3),
       (5, 'Costa Rica', 'San José', 4586353, true, '1945-11-2', 2),
       (6, 'The Netherlands', 'Amsterdam', 17734100, true, '1945-12-10', 1),
       (7, 'Republic of Korea', 'Seoul', 51744876, true, '1991-12-17', 4),
       (8, 'The Dominican Republic', 'Santo Domingo', 10694700, false, '1945-10-24', 2),
       (9, 'Peru', 'Lima', 34294231, false, '1945-10-31', 3),
       (10, 'Guatemala', 'Guatemala City', 17263239, false, '1945-11-21', 2),
       (11, 'United States of America', 'Washington, D.C.', 331893745, true, '1945-10-24', 2),
       (12, 'Vatican City State', 'Vatican City', 453, false, null, null);

Spring Boot executes both scripts in the proper order each time we run the tests.

The primary keys of the records are in the class DatasetConstants:

public class DatasetConstants {

    public static final Long UEFA_ID = 1L;
    public static final Long CONCACAF_ID = 2L;
    public static final Long CONMEBOL_ID = 3L;
    public static final Long AFC_ID = 4L;

...

JPA Entity Classes

In JPA we model the tables as entity classes. In short, an entity class represents a table (*) and its columns. Their instances are named entities and represente the records of the tables.

(*) To be rigorous, I point out that this statement is inaccurate in two cases: inheritance and secondary tables.

This entity class represents the countries table:

@Entity
@Table(name = "countries")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Country {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @Column(nullable = false, unique = true)
    private String name;

    @Column(nullable = false)
    private Integer population;

    @Column(nullable = false, unique = true)
    private String capital;

    private Boolean oecd;

    @Column(name = "united_nations_admission")
    private LocalDate unitedNationsAdmission;

    @ManyToOne(fetch = FetchType.LAZY, optional = true)
    @JoinColumn(name = "confederation_id")
    private Confederation confederation;

}

There are a lot of annotations! The first two belong to JPA. @Entity indicates that the class models a JPA entity, and @Table specifies the name of the represented table. Therefore Country represents the table countries. Without @Table, JPA considers that the table’s name matches the class’s.

The next four annotations are from Lombok. They request the generation of accessors (@Getter) and mutators (@Setter) methods for the fields, the no-args constructor (@NoArgsConstructor), and another constructor that takes all the fields (@AllArgsConstructor) following their declaration order. JPA only requires the no-args constructor. I’ve added them because they’ll help us to play with the entities. In a real project, create only the methods you need.

Let’s move on to fields:

  • The id field is the entity identifier, so it’s marked with @Id. It maps the table’s primary key. Every entity class must have an identifier. In our case, the database generates the identifier \ primary key using a sequence named countries_seq.
  • The name of the country and the capital city are text fields. @Column annotation indicates that the mapped columns have a unique value (unique = true) and mandatory (non-null value, nullable = false).
  • A boolean indicates whether the country is a member of the Organization for Economic Co-operation and Development (OECD). The field isn’t annotated—it doesn’t need any configuration.
  • The date of admission to the United Nations. Notice that the column name (united_nations_admission) and the attribute name (unitedNationsAdmission) differ. In these cases, the column name is set with the name attribute of @Column.
  • A lazy many-to-one association (@ManyToOne) with the FIFA soccer confederation to which the country is affiliated. The association is optional (optional = true, default value), which means that there might exist countries not affiliated to a confederation. @JoinColumn specifies the column with the foreign key that references the column with the primary key of the confederations table.

And speaking of the confederations table, the class that represents it:

@Entity
@Table(name = "confederations")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Confederation {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @Column(nullable = false, unique = true)
    private String name;

    @OneToMany(mappedBy = "confederation")
    private List<Country> countries;

}

The association between countries and confederations is bidirectional. This means the Country and Confederation have a field that links them: confederation in Country and the list countries in Confederation. Thus the association enables the navigation between both classes. Notice the arrows in the diagram below.

It would be enough to declare the association only in Country because that class represents the table containing the column with the foreign key. In the project the association is bidirectional for educational reasons, as we’ll see in the chapter about pagination. In that chapter I’ll explain why such bidirectional associations are sometimes dangerous. Spoiler: they can negatively affect performance. As a general rule, create only the necessary associations.

There’s another class, named User, that represents the users table. As I mentioned, it will appear in the last chapter.

Automatically Generated Classes

The project compilation creates several classes related to the entity classes in the /target/generated-sources/annotations directory.

Those classes constitute the JPA metamodel (Chapter 11) and the QueryDSL Q classes (Chapter 12). Without them, the project doesn’t compile.

If IntelliJ doesn’t generate the classes shown in the above screenshot, or the code can’t find them, reload the project with the “Reload All Maven Projects” button. It’s in the Maven tool window; you open it from the menu with “Window” -> “Tool Windows” -> “Maven”.

You may also force the rebuild of the entire project with the option “Build” -> “Rebuild Project”.

Spring Data, JPA, and Hibernate Integration

This task is child’s play: you don’t have to do anything! Spring Boot configures Spring integration with JPA and Hibernate, a transaction manager, and Spring Data JPA.

Logging with Slf4j and Logback

Spring Boot uses SLF4J as a logging API. This API has implementations called adapters that allow it to be used with various logging libraries. Adapters convert SLF4J method calls into calls to the underlying logging library, which does all the work. As a result, if your code uses SLF4J, you won’t have to rewrite it if you ever change the library behind the API.

Are you familiar with the above design? It follows the JDBC and JPA strategy I explained in the first chapter.

By default, Spring Boot relies on Logback as the logging library behind SLF4J. Logback is configured in the /src/main/resources/logback.xml file, but we can set up a basic configuration in the application.properties with the settings whose name begins with logging.

In the course we’ll review the SQL statements that Hibernate executes. This configuration prints them in the log output (pay attention to the Spring Boot version):

#SQL sentences
logging.level.org.hibernate.SQL=DEBUG

#query params Hibernate 5  & Spring Boot 2
#logging.level.org.hibernate.type=TRACE

#query params Hibernate 6  & Spring Boot 3
logging.level.org.hibernate.orm.jdbc.bind=TRACE

Note. Do you need more detailed monitoring of the SQL? Consider using P6Spy. You integrate this library into Spring Boot with this starter.

Testing with Spring Boot, JUnit 5, and AssertJ

We’ll write many tests to put Spring Data JPA capabilities into practice. Here’s a typical test class test from the sample project:

@SpringBootTest
class CountryDerivedQueryRepositoryTest {

    @Autowired
    private CountryDerivedQueryRepository countryRepository;

    @Test
    void testFindByCapitalIgnoreCase() {
        Optional<Country> spain = countryRepository.findByCapitalIgnoreCase("madrid");

        assertThat(spain).isNotEmpty();
        assertThat(spain.get().getId()).isEqualTo(SPAIN_ID);
    }

}

The @SpringBootTest annotation is the key. It means that the class contains tests that require bootstrapping Spring Boot so that we can inject beans, like Spring Data repositories.

By default, we develop tests in Spring Boot with the Jupiter library, which belongs to the JUnit 5 testing platform. The usual convention —I’ll follow it— is to refer to Jupiter tests as “JUnit 5 tests”.

A JUnit 5 test is a method annotated with @Test, non-private, and without a return value. The test in the example verifies a method (findByCapitalIgnoreCase(), line 9) we’ll write in Chapter 5 that finds a country using the name of its capital. If you don’t know, Madrid is Spain’s capital.

After calling the method under test, we should check that the method worked as expected. For that purpose, we write assertions,  code that checks logical conditions. As you can see, testFindByCapitalIgnoreCase() performs two assertions. The first one (line 11) checks that spain, the optional object returned by findByCapitalIgnoreCase(), is not empty. The second (line 12) verifies that spain contains the entity corresponding to Spain by checking the entity identifer.

I wrote the assertions with AssertJ, a library that provides a fluent API for creating readable, elegant, and expressive assertions. spring-boot-starter-test includes AssertJ, so we don’t have to add the dependency to the pom.

Running the Project

As I said, the class annotated with @SpringBootApplication runs the project. Yet if you execute the main method (SpringBootApp#main), you’ll only get Spring to start and stop. The project lacks, for instance, a REST API that keeps waiting for incoming HTTP requests. The executable code is the automated tests.

Let’s see how to run the tests.

Note: Make sure that Maven and your IDE compiles and runs the project with Java 17 or higher.

Development Environments

Select the classes or packages containing the tests you wish to run, open the selection context menu with the right mouse button, and find an option labeled “RUN” or similar.

The next image shows IntelliJ on the left and Eclipse on the right.

Your IDE probably has additional tools, such as keyboard shortcuts or toolbar buttons.

Maven Wrapper

With a development environment you already have the necessary resources to run the project. Still, I’ve included the Maven wrapper in the project. It’s a small tool composed of various files and scripts with everything you need to work with a specific version of Maven.

I recommend using the wrapper because you distribute the tool that builds the project along with the source code. Thus you build the project wherever you want without having to install Maven. Plus, you always use the exact Maven version your project requires.

With the wrapper, Maven is available via the mvnw.cmd (Windows) or mvnw (Linux) script. The commands are the same as the ones you would run without the wrapper script. The following, launched in a terminal located in the root directory of the project, compiles the project and performs all the tests:

mvnw test

You can specify the test classes you want to run:

mvnw test -Dtest=CountryRepositoryTest

and even individual tests:

mvnw test -Dtest=CountryRepositoryTest#testRepository

You’ll find all the options in the Maven Surefire plugin documentation. This plugin runs the tests.

Summary

The key points of the chapter:

  • The sample project for the course is a Maven project based on Spring Boot 3.1 and Java 17. You can import it into any IDE. It includes the Maven wrapper.
  • We simplify the code with Lombok annotations.
  • The database is HyperSQL, embedded and volatile. Spring Boot initializes it with the scripts /src/main/resources/schema.sql and /src/main/resources/data.sql.
  • We have two tables represented by the entity classes (@Entity) Country and Confederation.
  • Spring Boot configures and integrates HyperSQL, Spring Data, JPA, and Hibernate. You don’t have to do anything.
  • We’ll test the code we’ll write using JUnit 5 tests implemented within classes annotated with @SpringBootTest.

Source Code

The sample project is available on GitHub. For more information, consult this tutorial: How to import repositories from GitHub with Git, Eclipse, and Android StudioIntelliJ.



Other Posts in English

JSP and Spring Boot

Spring Framework: asynchronous methods with @Async, Future and TaskExecutor

Spring Framework: events handling

Spring Boot testing: Docker with Testcontainers and JUnit 5. MySQL and other images.

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.