© Adam L. Davis 2020
A. L. DavisSpring Quick Reference Guidehttps://doi.org/10.1007/978-1-4842-6144-6_6

6. Spring Data

Adam L. Davis1 
(1)
Oviedo, FL, USA
 

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.

Spring Data integrates well with the standard annotations under the “javax.persistence” package. For this chapter, assume a Customer entity class defined like the following:
import javax.persistence.*;
import lombok.*;
@Data
@Entity
@RequiredArgsConstructor
@Table("customers")
public class Customer {
  private @GeneratedValue @Id Long id;
  private final String firstname;
  @Column("surname")
  private final String lastname;
}

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.

Also, we will be using the following Course class:
import org.springframework.data.jpa.domain.AbstractPersistable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.NamedQuery;
@Entity
@NamedQuery(name = "Course.findByTheName",
    query = "from Course c where c.name = ?1")
public class Course extends AbstractPersistable<Long> {
    @Column(unique = true)
    private String name;
    private String subtitle;
    private String description;
    public Course() { this(null); }
    public Course(Long id) {
        this.setId(id);
    }
  // getters and setters omitted
}
Listing 6-1

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

Spring Data JDBC is similar to Spring Data JPA, building upon many of the same abstractions, except 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.

In a Maven project, put the following under dependencies:
<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-jdbc</artifactId>
  <version>2.0.1.RELEASE</version>
</dependency>
Or in a Gradle build, include the following under dependencies:
implementation 'org.springframework.data:spring-data-jdbc:2.0.1.RELEASE'

Use the @EnableJdbcRepositories annotation to enable them using Java configuration, for example, using a Java configuration class named CategoryConfiguration:

@Configuration
@EnableJdbcRepositories
public class 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

When either using Spring Data JDBC and you need custom queries or using Spring Data JPA and the built-in Spring Data conventions do not suffice, you can specify custom SQL (or JPQL when using Spring Data JPA) queries using the @Query annotation, for example:
@Query("SELECT * FROM customer WHERE lastname = :lastname")
List<Customer> findAllByLastname(@Param("lastname") String lastname);
@Query("SELECT firstname, lastname FROM Customer WHERE lastname = ?1")
Customer findFirstByLastname(String lastname);

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.

../images/498572_1_En_6_Chapter/498572_1_En_6_Figa_HTML.jpg 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.

You can also define modifying statements such as the following:
@Query("delete from Customer c where c.active = false")
void deleteInactiveCustomers();

Custom Queries in JPA

You can also define method signatures using a basic syntax, and Spring will implement them within Spring Data JPA. Examples are as follows:
  • 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

The following are examples of conditions allowed in custom query method expressions:
  • 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

For a more direct connection to a database, you can use org.springframework.jdbc.core.JdbcTemplate<T>.
  • 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

You can enable Spring Data repository proxy creation via Java or XML, for example:
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories
@Configuration
class DataConfig { //... }
Listing 6-2

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 .

To enable in Spring Data JPA in XML:
<beans xmlns:="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:jpa="http://www.springframework.org/schema/data/jpa"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/jpa
    https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
  <jpa:repositories base-package="com.acme.repositories"/>
</beans>

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.

For example, the following interface extends CrudRepository<T,ID> and adds a method for finding Customer entities by lastName:
@Repository
public interface PersonRepository extends CrudRepository<Customer, Long> {
  List<Customer> findByLastname(String lastname);
  // additional custom query methods go here
}
Listing 6-3

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:

public interface PagingAndSortingRepository<T, ID>
        extends CrudRepository<T, ID> {
  Iterable<T> findAll(Sort sort);
  Page<T> findAll(Pageable pageable);
}

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.

../images/498572_1_En_6_Chapter/498572_1_En_6_Figb_HTML.jpg 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.

For example, here is an annotated method definition of a query:
@Transactional(timeout = 10, readOnly = true,
    propagation = Propagation.REQUIRES_NEW)
Customer findByBirthdateAndLastname(LocalDate date, String lastname);

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.

../images/498572_1_En_6_Chapter/498572_1_En_6_Figc_HTML.jpg Transactions are only rolled back for unchecked Exceptions by default. You can change this by setting the rollbackFor property of the @Transactional annotation.

The different propagation settings available are as follows:
  • 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.

You can also set the isolation level of a Transaction to one of five different values (using, e.g., @Transaction(isolation = Isolation.READ_COMMITTED)):
  • 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).

    ../images/498572_1_En_6_Chapter/498572_1_En_6_Figd_HTML.jpg 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 Data R2DBC1 contains a wide range of features:
  • 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

Although the project is relatively young, existing drivers include the following as of writing (with groupId:artifactId names):
  • 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.

Spring Data R2DBC wraps R2DBC in a familiar fashion. You can create a Repository interface and extend ReactiveCrudRepository<T,ID>, and Spring will generate the implementations for you.
public interface PersonRepository
     extends ReactiveCrudRepository<Customer, Long> {
// additional custom query methods go here
}
Unlike a normal CrudRepository<T,ID>, the ReactiveCrudRepository<T,ID> methods all return reactive types like Mono and Flux (see Chapter 12 for more about these types). For example, here are some of the methods:
  • 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

You can also specify custom SQL queries using the @Query annotation just like for JPA or JDBC, for example:
@Query("SELECT * FROM customer WHERE lastname = :lastname")
Flux<Customer> findByLastname(String lastname);
@Query("SELECT firstname, lastname FROM Customer WHERE lastname = ?1")
Mono<Customer> findFirstByLastname(String lastname);

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).

../images/498572_1_En_6_Chapter/498572_1_En_6_Fige_HTML.jpg For more information, see the documentation for Spring Data R2DBC.2