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 <<
Contents
- Introduction
- Note About Versions (JEE vs. Jakarta EE)
- Get the Project
- Project Description
- Running the Project
- Summary
- 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.
Library | Version |
Spring Framework (core) | 6.1.3 |
Spring Data (Commons y JPA) | 3.2.2 |
JPA (Jakarta Persistance) | 3.1 |
Hibernate | 6.4.1 |
HyperSQL | 2.7.2 |
JUnit Jupiter | 5.10.1 |
Lombok | 1.18.30 |
AssertJ | 3.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 namedcountries_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 thename
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 theconfederations
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
andConfederation
. - 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
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.