Spring Data’s mission is to provide a familiar and consistent Spring-based programming model for data access while still retaining the special traits of the underlying data store.
Spring Data has support for every kind of data store, both relational and nonrelational (SQL and NoSQL), from JPA (Hibernate or TopLink) to Apache Cassandra and MongoDB to Redis. In this chapter, we will explore Spring Data JPA, JDBC, and R2DBC and how they support relational databases such as MariaDB, Oracle, or PostgreSQL.
Domain Entities
To get started in any Spring Data project, we need to define the domain classes or entities that our project depends upon. These are the domain objects that map to our database tables and are highly dependent upon the business domain of the application.
The @Entity annotation is necessary to mark this class for Spring Data as an entity class. The @Id annotation marks the field representing the primary key of the table. Optional annotations like @Table and @Column can be used to use names which do not match the class of field names, respectively. Note that we are using Lombok’s @Data and @RequiredArgsConstructor to remove the need for repetitive code such as getters, setters, toString, equals, hashCode, and constructors. In this case, since firstname and lastname are final, @RequiredArgsConstructor creates a constructor that sets those two values from two parameters.
Course.java
Note that we use @NamedQuery here to define a custom query directly on the entity class. We also are using @Column(unique = true) to specify that the name column is unique. Also, by extending AbstractPersistable<Long>, we inherit an Id property with a type of Long and denote this class as a database persistable entity to Spring Data. This is optional.
Course and Customer are deliberately defined in a different way to demonstrate some of the possible ways to define domain entities in Spring Data.
This book will refer back frequently to the domain of an online learning application that has Courses and Customers.
Spring Data JDBC
Has no lazy loading
Has no caching built-in
Is more simple
Uses the concept of Aggregate Root
Supports defining a query manually only as a String in a @Query annotation, not through a method name
An Aggregate Root is a Root entity that when you save, it saves all of its references as well, and upon editing all of its references will be deleted and reinserted.
Getting Started
To get started, include the spring-data-jdbc jar dependency in your project.
Use the @EnableJdbcRepositories annotation to enable them using Java configuration, for example, using a Java configuration class named CategoryConfiguration:
Defining a Repository
CRUD stands for “Create, Read, Update, Delete.” In Spring Data, the CrudRepository<T,ID> interface provides built-in methods for interacting with a persistent data store, such as a relational database.
To define your repository, create an interface and extend CrudRepository<T,ID> with the generic types defined where T is the type of your entity class and ID is the type of its identifier. Spring will automatically implement the repository for you, including the following methods (this is not an exhaustive list):
S save(S entity) – Saves the entity to the database
findAll() – Returns all of them
S findById(ID)
count()
delete(T)
existsById(ID)
Custom Queries
The findAllByLastname query would find all Customer entities by lastname. Spring Data JDBC only supports named params (like the preceding :lastname), whereas Spring Data JPA also supports indexed parameters (like the preceding ?1). The @Param annotation tells Spring Data the name of the query parameter.
Spring supports Java 8 and above parameter name discovery based on the -parameters compiler flag. By using this flag in your build (which Spring Boot handles for you), you can omit the @Param annotation for named parameters.
Custom Queries in JPA
findByX – Finds one entity by a given value or values; X is a condition (we will cover what types of conditions are allowed).
findByFirstname(String name) – In this example, Firstname is the property by which to search.
findByFirstnameAndLastname – You can use “And”, “Or”, and “Not”.
OrderByY – Orders by “Y” property.
findAllByX – Finds all records that match a condition.
countByX – Counts all the records that match a condition.
findTopN – Returns only the top N records.
Conditions
Name(String name), NameEquals(String name), NameIs(String name) – Is or Equals are supported but are also implied by default.
IdGreaterThan(Long num) – Where id is greater than the given num.
IdLessThan(Long num) – Where id is less than the given num.
DateLessThan(Date d) – Where date is less than the given date, d.
DateGreaterThan(Date d) – Where date is greater than the given date, d.
DateBetween(Date d1, Date d2) – Where date is greater than or equal to d1 and less than or equal to d2.
DateBefore(Date d), DateAfter(Date d) – Works similar to LessThan and GreaterThan but only for dates.
NameLike(String string) – Where name is like the given value, string.
NameStartingWith(String string) – Where name starts with the given value, string.
NameEndingWith(String string) – Where name ends with the given value, string.
NameContaining(String string) – Where name contains the given String.
NameIgnoreCase(String string) – Where the name equals the given value ignoring the case (case-insensitive matching).
AgeIn(Collection<Long> ages) – Where age matches any value in a given collection.
AgeNotIn(Collection<Long> ages) – Where age does not match any value in a given collection.
JdbcTemplate
void query(String sql, Object[] args, RowCallbackHandler rch) – This method takes an SQL query, any number of arguments as an Object array, and a callback that gets called for each row of results.
<T> List<T> query(String sql, Object[] args, RowMapper<T> rowMapper) – This method is the same as the previous except it takes a RowMapper<T> which converts rows into POJOs and returns a List of those POJOs.
Spring Data JPA
DataConfig.java
Spring will then automatically create proxy instances of all declared Repository interfaces at runtime (under the package of the DataConfig class). The preceding @EnableJpaRepositories annotation will enable JPA; there are also other flavors like @EnableMongoRepositories .
Paging and Sorting
You can create a Repository interface and extend CrudRepository<T,ID>, and Spring will generate implementations to the built-in method from CrudRepository<T,ID> for you as well as custom query methods that you define using Spring Data’s naming conventions.
PersonRepository.java
Using the @Repository annotation here is not necessary, but you may want to add it to remind everyone that this interface is proxied and the proxy exists as a Spring bean.
On top of the CrudRepository<T,ID>, there is a PagingAndSortingRepository<T,ID> abstraction that adds additional methods to ease paginated access to entities. It looks like the following:
You can also add Sort or Pageable parameters to custom methods to achieve sorting and pagination of results.
Transactions
Transactions are atomic units of work – typically on a database – that either complete fully or are rolled back (canceled) completely if something fails and can contain any number of statements. Spring can assist in handling transactions either programmatically or through annotation processing – with the latter being preferred.
To begin, make sure to include the required dependencies to your project and then enable Spring’s Transaction annotation processing. Note that in XML you would use <tx:annotation-driven/> or in Java use @EnableTransactionManagement to get annotation-based configuration working.
Then you can annotate a method (or class) with @Transactional. When annotating a class, all methods of that class will inherit those transaction settings. It will wrap each method in a Transaction using Spring’s proxy of your class.
Another consequence of using proxies is that methods are only wrapped in a Transaction when called externally. In other words, if one method of a class directly calls another method in the same class that has @Transactional on it, it will not invoke the proxy, and thus the Transaction will not be initiated (or handled according to the annotation settings).
You can also annotate an interface to have every method affected; however, this is not suggested by the Spring team since it will only work if the proxy directly implements the interface.
Transactions can be given timeouts measured in seconds. They can also be marked as “read only” and have different isolation levels, propagation settings, and other different transaction settings.
This would have a timeout of ten seconds. The readOnly qualifier gives a hint to the JDBC driver and might improve performance, but the behavior depends on the driver. The propagation is set to REQUIRES_NEW which is explained next.
Transactions are only rolled back for unchecked Exceptions by default. You can change this by setting the rollbackFor property of the @Transactional annotation.
REQUIRED – To either join an active transaction or to start a new one if the method gets called without a transaction (this is the default behavior).
SUPPORTS – To join an active transaction if one exists, otherwise without a transactional context.
MANDATORY – To join an active transaction if one exists or to throw an Exception if the method gets called without an active transaction.
NEVER – To throw an Exception if the method gets called in the context of an active transaction.
NOT_SUPPORTED – To suspend an active transaction (if one exists) and to execute the method without any transactional context.
REQUIRES_NEW – To always start a new transaction for this method. If the method gets called with an active transaction, that transaction is suspended, while this method is executed.
NESTED – To start a new transaction if the method gets called without an active transaction, and if it gets called with an active transaction, a new transaction is created that wraps only this method execution.
DEFAULT – This is the default and depends on what the default isolation level is for your database.
READ_UNCOMMITTED – This is the lowest level and allows for the most concurrency; however, it suffers from dirty reads, nonrepeatable reads, and phantom reads.
READ_COMMITTED – This is the second lowest level and prevents dirty reads, but still suffers from nonrepeatable reads and phantom reads.
REPEATABLE_READ – This level prevents dirty reads and nonrepeatable reads, with the trade-off of allowing less concurrency, but still suffers from phantom reads.
SERIALIZABLE – This is the highest level of isolation and prevents all concurrency side effects, with the trade-off of very low concurrency (only one serializable operation can happen at a time).
To understand these isolation levels, you need to understand the setbacks of concurrent transactions (dirty reads, nonrepeatable reads, and phantom reads). A dirty read is when a single transaction reads data from another concurrent transaction that has not yet committed. A nonrepeatable read is one where another transaction commits new data that is read after already reading different data earlier. A phantom read happens when you get different rows due to another transaction adding or removing rows during the current transaction.
Spring Data R2DBC
R2DBC stands for Reactive Relational Database Connectivity. It is an API for interacting with relational databases like PostgreSQL, H2, and Microsoft SQL asynchronously using reactive types.
Spring configuration support
A DatabaseClient helper interface with a builder that assists in performing common R2DBC operations with integrated object mapping between rows and POJOs
Exception translation into Spring’s Data Access Exceptions
Feature-rich object mapping integrated with Spring’s Conversion Service
Annotation-based mapping metadata that is extensible to support other metadata formats
Automatic implementation of Repository<T,ID> interfaces, including support for custom query methods
Postgres (io.r2dbc:r2dbc-postgresql)
H2 (io.r2dbc:r2dbc-h2)
Microsoft SQL Server (io.r2dbc:r2dbc-mssql)
MySQL (dev.miku:r2dbc-mysql)
Spring Data has an R2DBC integration, and there is a spring-boot-starter-data-r2dbc.
Mono<Void> delete(T entity) – Deletes the given entity from the database
Flux<T> findAll() – Returns all instances of the type
Mono<T> findById(org.reactivestreams.Publisher<ID> id) – Retrieves an entity by its ID, which is supplied by a Publisher
Mono<S> save(S entity) – Saves a given entity
Flux<S> saveAll(Iterable<S> entities) – Saves all given entities
Flux<S> saveAll(org.reactivestreams.Publisher<S> entityStream) – Saves all given entities from a given Publisher
Custom Reactive Queries
Kotlin Support
Spring Data R2DBC supports Kotlin 1.3.x in many ways.
It requires kotlin-stdlib (or one of its variants, such as kotlin-stdlib-jdk8) and kotlin-reflect to be present on the classpath (which is provided by default if you bootstrap a Kotlin project via https://start.spring.io).
For more information, see the documentation for Spring Data R2DBC.2