09 Data Abstraction How to Carry Out Unified Data Access Abstraction With Spring Data

09 Data Abstraction - How to Carry out Unified Data Access Abstraction with Spring Data #

In fact, JdbcTemplate is a relatively low-level utility class. As one of the most important basic functionalities in system development, the development of the data access layer component has been further simplified in Spring Boot, leveraging another important member of the Spring family called Spring Data.

In the previous lessons, we introduced the JdbcTemplate class in the Spring framework for accessing relational databases. Today, we will discuss the data access methods provided by the Spring Data framework.

Spring Data is an open-source framework dedicated to data access in the Spring family. Its core concept is to support resource configuration for all storage media to achieve data access. We know that data access requires mapping between domain objects and stored data, and providing access entry points. Based on the Repository architecture pattern, Spring Data abstracts a unified data access method.

The abstraction of the data access process by Spring Data is mainly manifested in two aspects: ① providing a set of Repository interface definitions and implementations; ② implementing various diversified query supports. Let’s take a closer look at each of them.

Repository Interface and Implementation #

The Repository interface is the highest-level abstraction for data access in Spring Data. The interface is defined as follows:

public interface Repository<T, ID> {

}

In the above code, we can see that the Repository interface is just an empty interface, specifying the type of the domain entity object and the ID through generics. In Spring Data, there are a large number of sub-interfaces and implementation classes of the Repository interface. The class hierarchy of this interface is shown below:

image

Partial class hierarchy of the Repository interface

As we can see, the CrudRepository interface is the most common extension of the Repository interface, adding CRUD operation functions for domain entities. The specific definition is shown in the following code:

public interface CrudRepository<T, ID> extends Repository<T, ID> {

  <S extends T> S save(S entity);

  <S extends T> Iterable<S> saveAll(Iterable<S> entities);

  Optional<T> findById(ID id);

  boolean existsById(ID id);

  Iterable<T> findAll();

  Iterable<T> findAllById(Iterable<ID> ids);

  long count();

  void deleteById(ID id);

  void delete(T entity);

  void deleteAll(Iterable<? extends T> entities);

  void deleteAll();

}

These methods are self-explanatory. We can see that the CrudRepository interface provides common operations such as saving a single entity, saving a collection, finding an entity by id, determining the existence of an entity by id, querying all entities, querying the number of entities, deleting an entity by id, deleting a collection of entities, and deleting all entities. Let’s take a closer look at the implementation process of some of these methods.

In the implementation process, we first need to pay attention to the most basic save method. By looking at the class hierarchy of CrudRepository, we found a implementation class called SimpleJpaRepository, which is obviously a data access class for relational databases implemented based on the JPA specification.

The save method is shown in the following code:

private final JpaEntityInformation<T, ?> entityInformation;

private final EntityManager em;

 

@Transactional

public <S extends T> S save(S entity) {

    if (entityInformation.isNew(entity)) {

        em.persist(entity);

        return entity;

    } else {

        return em.merge(entity);

    }

}

Obviously, the above save method depends on the EntityManager in the JPA specification. When it detects that the entity passed in is a new object, it will call the persist method of the EntityManager. Otherwise, it will use the merge method with the entity. We will discuss the JPA specification and EntityManager in detail in the next lesson.

Let’s take a look at the findOne method, which is used to query an entity by id, as shown in the following code:

public T findOne(ID id) {

    Assert.notNull(id, ID_MUST_NOT_BE_NULL);

    Class<T> domainType = getDomainClass();

    if (metadata == null) {

        return em.find(domainType, id);

    }

    LockModeType type = metadata.getLockModeType();

    Map<String, Object> hints = getQueryHints();

    return type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints);

}

During the execution of the query, the findOne method will call the find method of the EntityManager to find the target object based on the type of the domain entity. It is worth noting that this method also uses some metadata Metadata and hint mechanisms that can change the normal SQL execution behavior.

Diversified Query Support #

In everyday development, the number of data query operations is much higher than data insertion, deletion, and modification operations. Therefore, in Spring Data, in addition to providing default CRUD operations for domain objects, we also need to abstract the query scenarios to a high level. In real business scenarios, the most typical query operations are the @Query annotation and the method name-derived query mechanism.

@Query Annotation #

We can directly embed the query statement and conditions into the code using the @Query annotation, providing powerful functionalities similar to ORM frameworks.

Here is a typical example of using the @Query annotation for querying:

public interface AccountRepository extends JpaRepository<Account, 

    Long> {

     

  @Query("select a from Account a where a.userName = ?1") 

  Account findByUserName(String userName);

}

Notice that the @Query annotation here uses syntax similar to SQL statements, which can automatically handle the mapping between the domain object Account and the database data. Since we are using JpaRepository, this SQL-like syntax is actually a JPA query language called JPQL (Java Persistence Query Language).

The basic syntax of JPQL is as follows:

SELECT clause FROM clause 

[WHERE clause] 

[GROUP BY clause]

[HAVING clause] 

[ORDER BY clause]
JPQL statements are very similar to native SQL statements, with the only difference being that JPQL's FROM statement is followed by an object, while native SQL corresponds to fields in a database table.

After introducing JPQL, let's go back to the definition of the @Query annotation, which is located in the org.springframework.data.jpa.repository package, as shown below:

```java
package org.springframework.data.jpa.repository;

public @interface Query {

    String value() default "";
    
    String countQuery() default "";
    
    String countProjection() default "";
    
    boolean nativeQuery() default false;
    
    String name() default "";
    
    String countName() default "";
    
}

The most commonly used attribute in the @Query annotation is the value attribute, which was used in the previous example with the JPQL statement. Of course, if we set nativeQuery to true, the value attribute needs to specify the specific native SQL statement.

Please note that there is a batch of @Query annotations in Spring Data that correspond to different persistence media. For example, there is a @Query annotation in MongoDB, but it is located in the org.springframework.data.mongodb.repository package and is defined as follows:

package org.springframework.data.mongodb.repository;

public @interface Query {

    String value() default "";
    
    String fields() default "";
    
    boolean count() default false;
    
    boolean exists() default false;
    
    boolean delete() default false;
    
}

Unlike the @Query annotation for JPA, the value of the @Query annotation in MongoDB is a JSON string that specifies the object conditions to be queried.

Method Name Derived Queries #

Method name derived queries are also one of Spring Data’s querying features. By using query fields and parameters directly in the method name, Spring Data can automatically recognize the corresponding query conditions and assemble the corresponding query statement. A typical example is shown below:

public interface AccountRepository extends JpaRepository<Account, Long> {

    List<Account> findByFirstNameAndLastName(String firstName, String lastName);
}

In the above example, by using the method name findByFirstNameAndLastName, which conforms to the common semantics, and passing the corresponding parameters in the parameter list in the order and names specified in the method name (i.e., the first parameter is firstName, and the second parameter is lastName), Spring Data can automatically assemble the SQL statement to achieve derived queries. Isn’t it magical?

To use method name derived queries, we need to constrain the method names defined in the repository.

First, we need to specify some query keywords. The common keywords are shown in the following table:

Lark20201215-174017.png

Query keywords in method name derived queries

With these query keywords, we also need to specify the query fields and some limiting conditions in the method name. For example, in the previous example, we only queried based on the “firstName” and “lastName” fields.

In fact, we can query a lot of content. The following table lists more examples of method name derived queries for your reference.

Lark20201215-174023.png

Examples of method name derived queries

In Spring Data, method name derived queries have very powerful functionality, and the ones listed in the table are only a small part of the complete functionality.

At this point, you may ask a question: If we specify both the @Query annotation and method name derived queries in a repository, which one will Spring Data execute specifically? To answer this question, we need to have a certain understanding of query strategies.

In Spring Data, the query strategy is defined in QueryLookupStrategy, as shown in the following code:

public interface QueryLookupStrategy {

    public static enum Key {

        CREATE, USE_DECLARED_QUERY, CREATE_IF_NOT_FOUND;

        public static Key create(String xml) {

            if (!StringUtils.hasText(xml)) {

                return null;

            }

            return valueOf(xml.toUpperCase(Locale.US).replace("-", "_"));

        }

    }

    RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries);

}

From the above code, we can see that QueryLookupStrategy is divided into three types: CREATE, USE_DECLARED_QUERY, and CREATE_IF_NOT_FOUND.

The CREATE strategy refers to a query strategy created directly based on the method name, which means using the method name derived queries introduced earlier.

The USE_DECLARED_QUERY strategy refers to the declared strategy, which mainly uses the @Query annotation. If there is no @Query annotation, the system will throw an exception.

The last strategy, CREATE_IF_NOT_FOUND, can be understood as a compatible version of the @Query annotation and method name derived queries. Please note that Spring Data defaults to using the CREATE_IF_NOT_FOUND strategy, which means that the system will first look for the @Query annotation. If it is not found, it will then look for a query matching the method name.

Components in Spring Data #

Spring Data supports accessing multiple data storage media, manifested as providing a series of default repositories, including JPA/JDBC repositories for relational databases, repositories for NoSQL databases like MongoDB, Neo4j, Redis, repositories for accessing big data with Hadoop, and even repositories for system integration, such as Spring Batch and Spring Integration.

On the official website of Spring Data https://spring.io/projects/spring-data, a list of all the components it provides is shown as follows:

image

List of components provided by Spring Data (from the Spring Data official website)

According to the official website, the components in Spring Data can be divided into four categories: Main modules, Community modules, Related modules, and Modules in Incubation. For example, the Repository and diverse querying functions introduced earlier are part of the core module called Spring Data Commons.

Here, I would like to emphasize the Modules in Incubation, which currently only includes one component - Spring Data R2DBC. R2DBC stands for Reactive Relational Database Connectivity and represents reactive database connectivity, which is equivalent to the JDBC specification in the reactive data access field.

Summary and Preview #

Data access is the foundation of all application systems. As an integrated development framework, Spring Boot provides the Spring Data component to abstract the data access process. Based on the Repository architecture pattern, Spring Data provides a series of utility methods for performing CRUD operations, especially refined designs for the most commonly used query operations, making the development process simpler and more efficient.