03 | Naming syntax and parameters of Defining Query Methods

The biggest feature of Spring Data JPA is the use of Method Name Defining Query Methods (Defining Query Methods) to perform CRUD operations. I will explain this in detail in this lesson.

At work, do you often worry about the semantics and naming conventions of method names? Do you need to write various SQL statements for different query conditions? Should I write a super general query method or SQL for the same entity query? If other development colleagues don’t check the SQL statement you wrote, but look directly at the method name, they don’t know what you want to check and are frustrated?

Spring Data JPA’s Defining Query Methods (DQM) can solve the above problems well through method names and parameters. It can also make the semantics of our method names clearer and improve development efficiency a lot. There are 2 types of DQM syntax, which can solve the above problems, as follows:

  • One is to implement it directly through the method name, which is also the key content that will be introduced in detail in this lesson;
  • The other is to manually define @Query on the method, which will be introduced in detail in Lesson 05 “What problem does @Query help us solve? When should we choose @Query?”

Below I will explain Defining Query Methods in detail from 6 aspects. Let’s first analyze the “configuration and usage of defining query methods”. This is the syntax that must be mastered in Defining Query Methods.

Define the configuration and usage of query methods

If you want to implement CRUD operations, the conventional approach is to write a lot of SQL statements. But in JPA, you only need to inherit any Repository interface or sub-interface in Spring Data Common, and then you can implement it directly through the method name. Isn’t it magical? Let’s look at the specific usage steps below.

Step 1. The UserRepository of the User entity inherits the Repository interface in Spring Data Common:

Copy code

interface UserRepository extends CrudRepository<User, Long> {
     User findByEmailAddress(String emailAddress);
}

Step 2. For the Service layer, you can directly use the UserRepository interface:

Copy code

@Service
public class UserServiceImpl{
    @Autowired
    UserRepository userRepository;
    public void testJpa() {
        userRepository.deleteAll();
        userRepository.findAll();
        userRepository.findByEmailAddress("[email protected]");
    }

At this time, you can directly call all the interface methods exposed in CrudRepository, as well as the methods defined in UserRepository, without writing any SQL statements or any implementation methods. Through the above two steps, we have completed the basic use of Defining Query Methods (DQM). Let’s look at another situation: Selective Exposure Method.

However, sometimes if we don’t want to expose all the methods in CrudRepository, we can directly inherit the interface of those methods that we think need to be exposed. If UserRepository only wants to expose findOne and save, and does not allow any User operations other than these two methods, the method is as follows.

We selectively expose CRUD methods, directly inherit Repository (because there are no methods in it), and copy the save and findOne methods in CrudRepository to our own MyBaseRepository interface. The code is as follows:

Copy code

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends Repository<T, ID> {
    T findOne(ID id);
    T save(T entity);
}
interface UserRepository extends MyBaseRepository<User, Long> {
     User findByEmailAddress(String emailAddress);
}

In this way, there are only three methods that can be called in the Service layer: findOne, save, and findByEmailAddress. There will be no more methods. We can selectively expose any implemented method in SimpleJpaRepository.

To sum up, the following two conclusions can be drawn:

  • The MyRepository Extends Repository interface can implement the functions of Defining Query Methods;
  • Inheriting the sub-interfaces of other Repositories or customizing sub-interfaces can selectively expose the basic public methods implemented in SimpleJpaRepository.

In daily work, you can achieve the purpose of CRUD through the method name, or by adding @Query annotation to the defined method name, and Spring provides us with two switching methods. Next we will talk about “method query strategy settings”.

Method query strategy settings

At present, we have not encountered the situation of modifying the default policy in actual production, but we must know that there is such a configuration method and be aware of it, so that we can know why the method name is OK, and @Query is also OK. Configure the query strategy of the method through the @EnableJpaRepositories annotation. The detailed configuration method is as follows:

Copy code

@EnableJpaRepositories(queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)

Among them, QueryLookupStrategy.Key has a total of 3 values, as follows:

  • Create: Create directly based on the method name. The rule is to try based on the construction of the method name. The general method is to remove a given set of known prefixes from the method name and parse the rest of the method. part. If the method name does not comply with the rules, an exception will be reported when starting. In this case, it can be understood that even if @Query is configured, it is useless.
  • USE_DECLARED_QUERY: Created in declaration mode. When starting, it will try to find a declared query. If it is not found, an exception will be thrown. It can be understood that @Query must be configured.
  • CREATE_IF_NOT_FOUND: This is the default. Unless there are special requirements, it can be understood that this is a compatible version of the above two methods. First use the declaration method (@Query) to search. If no query matching the method is found, create a query using the method name creation rule of Create; if neither of these is satisfied, an error will be reported at startup.

Taking the Spring Boot project as an example, change its configuration as follows:

Copy code

@EnableJpaRepositories(queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
public class Example1Application {
   public static void main(String[] args) {
      SpringApplication.run(Example1Application.class, args);
   }
}

The above is the query strategy setting of the method, which is very simple. Next, let’s talk about “Defining Query Method (DQM) syntax”, which is the detailed syntax that can make the method effective.

Defining Query Method (DQM) syntax

The syntax is: the method name with query function consists of query strategy (keyword) + query field + some restrictive conditions. It has the characteristics of clear semantics and complete functions. 80% of API queries in our actual work can be easily implemented.

Let’s look at a more complicated example, which is an example with more and conditions, distinct or sorting, and ignoring case. The following code defines PersonRepository, which we can use directly in the service layer, as shown below:

Copy code

interface PersonRepository extends Repository<User, Long> {
   // Query relationship of and
   List<User> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
   // Contains distinct deduplication, sql syntax of or
   List<User> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
   // Ignore case based on lastname field query
   List<User> findByLastnameIgnoreCase(String lastname);
   // Query equal based on lastname and firstname and ignore case
   List<User> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
  // Sort the query results according to lastname, in positive order
   List<User> findByLastnameOrderByFirstnameAsc(String lastname);
  // Sort the query results according to lastname, in reverse order
   List<User> findByLastnameOrderByFirstnameDesc(String lastname);
}

The following table is a list of commonly used keywords in the above DQM method syntax, which is convenient for you to quickly check and meet more complex scenarios in actual code:

Lark20200918-182821.png

To sum up, let’s summarize 3 points of experience:

  • The expression of the method name is usually a combination of entity attribute connection operators, such as And, or, Between, LessThan, GreaterThan, Like and other attribute connection operation expressions. Different databases (NoSQL, MySQL) may produce different effects. If If we encounter a problem, we can open SQL log observation.
  • IgnoreCase can be used for a single attribute (such as findByLastnameIgnoreCase(…)), or it can ignore case for all entity attributes in the query condition (all attributes must be in the String case, such as findByLastnameAndFirstnameAllIgnoreCase(…)).
  • OrderBy can provide a direction (Asc or Desc) in the sorting of certain attributes, which is called static sorting. It can also implement a query method for dynamic sorting of specified fields through a convenient parameter Sort (such as repository.findAll(Sort.by(Sort .Direction.ASC, “myField”))).

We see that although most of the tables above are methods starting with find, in addition, JPA also supports prefixes such as read, get, query, stream, count, exists, delete, remove, etc., just like the literal meaning. Let’s take a look at the examples of count, delete, and remove. Other prefixes can be used to draw inferences. The example code is as follows:

Copy code

interface UserRepository extends CrudRepository<User, Long> {
     long countByLastname(String lastname);//Query the total number
     long deleteByLastname(String lastname);//Delete based on a field and return the number of deleted rows
     List<User> removeByLastname(String lastname);//Delete a bunch of Users based on Lastname and return the deleted Users
}

Sometimes as the version is updated, there will be more syntax support, or the syntax may be different in different versions. Let’s take a look at the syntax mentioned above through the source code. Interested students can go to the class org.springframework.data.repository.query.parser.PartTree to view the logic and processing methods of the relevant source code. The key source code is as follows:

Drawing 0.png

According to the source code, we can also analyze that the query method contains other expressions, such as find, count, delete, exist and other keywords that are matched by regular expressions before by.

Drawing 1.png

It can be seen from this that the keywords in our method are not filled in randomly, but are defined for us by the enumeration. Next, open the source code of the enumeration class Type and take a look, it will be clearer than anything else.

Copy code

public static enum Type {
    BETWEEN(2, new String[]{"IsBetween", "Between"}),
    IS_NOT_NULL(0, new String[]{"IsNotNull", "NotNull"}),
    IS_NULL(0, new String[]{"IsNull", "Null"}),
    LESS_THAN(new String[]{"IsLessThan", "LessThan"}),
    LESS_THAN_EQUAL(new String[]{"IsLessThanEqual", "LessThanEqual"}),
    GREATER_THAN(new String[]{"IsGreaterThan", "GreaterThan"}),
    GREATER_THAN_EQUAL(new String[]{"IsGreaterThanEqual", "GreaterThanEqual"}),
    BEFORE(new String[]{"IsBefore", "Before"}),
    AFTER(new String[]{"IsAfter", "After"}),
    NOT_LIKE(new String[]{"IsNotLike", "NotLike"}),
    LIKE(new String[]{"IsLike", "Like"}),
    STARTING_WITH(new String[]{"IsStartingWith", "StartingWith", "StartsWith"}),
    ENDING_WITH(new String[]{"IsEndingWith", "EndingWith", "EndsWith"}),
    IS_NOT_EMPTY(0, new String[]{"IsNotEmpty", "NotEmpty"}),
    IS_EMPTY(0, new String[]{"IsEmpty", "Empty"}),
    NOT_CONTAINING(new String[]{"IsNotContaining", "NotContaining", "NotContains"}),
    CONTAINING(new String[]{"IsContaining", "Containing", "Contains"}),
    NOT_IN(new String[]{"IsNotIn", "NotIn"}),
    IN(new String[]{"IsIn", "In"}),
    NEAR(new String[]{"IsNear", "Near"}),
    WITHIN(new String[]{"IsWithin", "Within"}),
    REGEX(new String[]{"MatchesRegex", "Matches", "Regex"}),
    EXISTS(0, new String[]{"Exists"}),
    TRUE(0, new String[]{"IsTrue", "True"}),
    FALSE(0, new String[]{"IsFalse", "False"}),
    NEGATING_SIMPLE_PROPERTY(new String[]{"IsNot", "Not"}),
    SIMPLE_PROPERTY(new String[]{"Is", "Equals"});
....}

By looking at the source code, you can know which logical keywords are supported by the framework, such as NotIn, Like, In, Exists, etc. This is sometimes more accurate and faster than checking documents and blogs written by anyone. Okay, the above introduces the basic expressions of aspect names. I hope you can use them flexibly in your work and draw inferences from one example. Next we talk about specific types of parameters: Sort sorting and Pageable paging, which are essential skills for paging and sorting.

Specific types of parameters: Sort and Pageable

In order to facilitate our sorting and paging, Spring Data JPA supports two special types of parameters: Sort and Pageable.

Sort can implement dynamic sorting when querying. Let’s take a look at its source code:

Copy code

public Sort(Direction direction, String... properties) {
   this(direction, properties == null ? new ArrayList<>() : Arrays.asList(properties));
}

Sort determines the sorting direction of our fields (ASC forward order, DESC reverse order).
Pageable can achieve dual effects of paging and dynamic sorting when querying. Let’s take a look at the Structure of Pageable, as shown in the following figure:

Drawing 2.png

We found that Pageable is an interface that contains common paging method sorting, current page, next row, current pointer, total number of pages, page number, pageSize, etc.

How to use Pageable and Sort in query method? The following code defines an instance of querying User paging and sorting based on Lastname. This code is a method defined in the UserRepository interface:

Copy code

Page<User> findByLastname(String lastname, Pageable pageable);//Query User based on paging parameters and return a Page (detailed explanation in the next lesson) object with paging results (Method 1)
Slice<User> findByLastname(String lastname, Pageable pageable);//We return a Slice user result based on the paging parameters (Method 2)
List<User> findByLastname(String lastname, Sort sort);//Return a List based on the sorting results (Method 3)
List<User> findByLastname(String lastname, Pageable pageable);//Return a List object according to the paging parameters (Method 4)

Method 1: Allows passing the org.springframework.data.domain.Pageable instance to the query method, adding paging parameters to the statically defined query, and knowing the available elements and pages through the results returned by Page total. This paging query method may be expensive (a count SQL statement will be executed by default), so you must consider the usage scenario when using it.

Method 2: The returned result is Slice, because we only know whether the next Slice is available, but not the count, so when querying a larger result set, we only know that the data is enough, that is to say When used in business scenarios, you don’t need to care about how many pages there are.

Method 3: If you only need sorting, you need to add a parameter to the org.springframework.data.domain.Sort parameter. As seen above, it is possible to just return a List.

Method 4: Sorting options are also handled through Pageable instances, in which case Page will not create the additional metadata required to build the actual instance (i.e. no need to calculate and query paging related data), It is only used to limit the query to entities within a given range.

So how to use it? Let’s take a look at the source code again, which is the implementation class of Pageable, as shown in the figure below:

Drawing 3.png

It can be seen that we can construct page numbers, page sizes, sorting, etc. through several of static methods (polymorphism) provided in PageRequest. Let’s take a look at how it’s written in use, as follows:

Copy code

//Query the first page of lastname=jk in user. The size of each page is 20 items; and the total number of pages of information will be returned.
Page<User> users = userRepository.findByLastname("jk",PageRequest.of(1, 20));
//Query the 20 pieces of data on the first page of lastname=jk in user. I don’t know how many pieces there are in total.
Slice<User> users = userRepository.findByLastname("jk",PageRequest.of(1, 20));
//Query the User data of lastname=jk in all users, and return the List in positive order by name
List<User> users = userRepository.findByLastname("jk",new Sort(Sort.Direction.ASC, "name"))
//Query the first one hundred User data in reverse order of createdAt
List<User> users = userRepository.findByLastname("jk",PageRequest.of(0, 100, Sort.Direction.DESC, "createdAt"));

The application scenarios of paging and sorting are explained above. In actual work, if you encounter a situation where you don’t know how to pass parameters, you can take a look at the source code, because Java is type-safe. Next, we will explain “limiting query results to First and Top”, which is another way of expressing paging.

Limit query results to First and Top

Sometimes we want to directly query the first few pieces of data without dynamic sorting, so we can simply use the First and Top keywords in the method name to limit the number of items returned.

Let’s take a look at some restrictions that can be defined in userRepository on the use of returned results. Add the keywords First and Top to limit the query results to the query method.

Copy code

User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
List<User> findDistinctUserTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);

in:

  • When the query method uses First or Top, the value can be appended to First or Top to specify the size of the maximum result returned;
  • If the number is omitted, the result size is assumed to be 1;
  • Limit expressions also support the Distinct keyword;
  • Supports packaging results into Optional (detailed in the next lesson).
  • If you pass Pageable as a parameter, the number after Top and First shall prevail, i.e. paging will be applied in limiting the results.

The use of the First and Top keywords is very simple and can make the semantics of our method names clearer. Next, let’s talk about the support for NULL.

@NonNull, @NonNullApi, @Nullable

Starting from Spring Data 2.0, JPA has added @NonNull @NonNullApi @Nullable, which supports null parameters and return results.

  • @NonNullApi: Used at the package level to declare parameters and return values. The default behavior is not to accept or generate null values.
  • @NonNull: Used for parameters or return values that cannot be null (not required on parameters and return values where @NonNullApi applies).
  • @Nullable: used for parameters or return values that can be null.

I make the following statement in the package-info.java class of the package where my Repository is located:

Copy code

@org.springframework.lang.NonNullApi
package com.myrespository;

The UserRepository below myrespository is implemented as follows:

Copy code

package com.myrespository;
import org.springframework.lang.Nullable;
interface UserRepository extends Repository<User, Long> {
  User getByEmailAddress(EmailAddress emailAddress);
}

At this time, an exception will be thrown when the emailAddress parameter is null, and an exception will be thrown when the return result is null. Because we specified NonNullApi in the package’s package-info.java, all return results and parameters cannot be Null.

Copy code

 @Nullable
  User findByEmailAddress(@Nullable EmailAddress emailAdress);//When we add the @Nullable annotation, the parameters and return results will be allowed to be null at this time;
  Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); //The return result is allowed to be null, and the parameters are not allowed to be null.

The above is the overall study of the method names and paging parameters of Defining Query Methods.

Some thoughts for us

We have learned the syntax of Defining Query Methods and the naming conventions it expresses. In actual work, we can also apply the mandatory conventions of method names (method naming conventions defined in the very semantic respository) to the controller and service layers. After all are unified in this way, a lot of communication costs can be reduced.

Can we apply and extend the repository base class in Spring Data Common to the service layer? Can I also create my own baseService? Let’s look at the following practical example:

Copy code

public interface BaseService<T, ID> {
    Class<T> getDomainClass();
    <S extends T> S save(S entity);
    <S extends T> List<S> saveAll(Iterable<S> entities);
    void delete(T entity);
    void deleteById(ID id);
    void deleteAll();
    void deleteAll(Iterable<? extends T> entities);
    void deleteInBatch(Iterable<T> entities);
    void deleteAllInBatch();
    T getOne(ID id);
    <S extends T> Optional<S> findOne(Example<S> example);
    Optional<T> findById(ID id);
    List<T> findAll();
    List<T> findAll(Sort sort);
    Page<T> findAll(Pageable pageable);
    <S extends T> List<S> findAll(Example<S> example);
    <S extends T> List<S> findAll(Example<S> example, Sort sort);
    <S extends T> Page<S> findAll(Example<S> example, Pageable pageable);
    List<T> findAllById(Iterable<ID> ids);
    long count();
    <S extends T> long count(Example<S> example);
    <S extends T> boolean exists(Example<S> example);
    boolean existsById(ID id);
    void flush();
    <S extends T> S saveAndFlush(S entity);
}

We imitated the JpaRepository interface and customized a BaseService of our own, declaring commonly used CRUD operations. The above code is production code and can be used as a reference. Of course, we can also create our own PagingAndSortingService, ComplexityService, SampleService, etc. to divide different service interfaces for service subclasses to inherit for different purposes.

Let’s imitate a SimpleJpaRepository again to implement our own BaseService implementation class.

Copy code

public class BaseServiceImpl<T, ID, R extends JpaRepository<T, ID>> implements BaseService<T, ID> {
    private static final Map<Class, Class> DOMAIN_CLASS_CACHE = new ConcurrentHashMap<>();
    private final R repository;
    public BaseServiceImpl(R repository) {
        this.repository = repository;
    }
    @Override
    public Class<T> getDomainClass() {
        Class thisClass = getClass();
        Class<T> domainClass = DOMAIN_CLASS_CACHE.get(thisClass);
        if (Objects.isNull(domainClass)) {
            domainClass = GenericsUtils.getGenericClass(thisClass, 0);
            DOMAIN_CLASS_CACHE.putIfAbsent(thisClass, domainClass);
        }
        return domainClass;
    }
    protected R getRepository() {
        return repository;
    }
    @Override
    public <S extends T> S save(S entity) {
        return repository.save(entity);
    }
    @Override
    public <S extends T> List<S> saveAll(Iterable<S> entities) {
        return repository.saveAll(entities);
    }
    @Override
    public void delete(T entity) {
        repository.delete(entity);
    }
    @Override
    public void deleteById(ID id) {
        repository.deleteById(id);
    }
    @Override
    public void deleteAll() {
        repository.deleteAll();
    }
    @Override
    public void deleteAll(Iterable<? extends T> entities) {
        repository.deleteAll(entities);
    }
    @Override
    public void deleteInBatch(Iterable<T> entities) {
        repository.deleteInBatch(entities);
    }
    @Override
    public void deleteAllInBatch() {
        repository.deleteAllInBatch();
    }
    @Override
    public T getOne(ID id) {
        return repository.getOne(id);
    }
    @Override
    public <S extends T> Optional<S> findOne(Example<S> example) {
        return repository.findOne(example);
    }
    @Override
    public Optional<T> findById(ID id) {
        return repository.findById(id);
    }
    @Override
    public List<T> findAll() {
        return repository.findAll();
    }
    @Override
    public List<T> findAll(Sort sort) {
        return repository.findAll(sort);
    }
    @Override
    public Page<T> findAll(Pageable pageable) {
        return repository.findAll(pageable);
    }
    @Override
    public <S extends T> List<S> findAll(Example<S> example) {
        return repository.findAll(example);
    }
    @Override
    public <S extends T> List<S> findAll(Example<S> example, Sort sort) {
        return repository.findAll(example, sort);
    }
    @Override
    public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
        return repository.findAll(example, pageable);
    }
    @Override
    public List<T> findAllById(Iterable<ID> ids) {
        return repository.findAllById(ids);
    }
    @Override
    public long count() {
        return repository.count();
    }
    @Override
    public <S extends T> long count(Example<S> example) {
        return repository.count(example);
    }
    @Override
    public <S extends T> boolean exists(Example<S> example) {
        return repository.exists(example);
    }
    @Override
    public boolean existsById(ID id) {
        return repository.existsById(id);
    }
    @Override
    public void flush() {
        repository.flush();
    }
    @Override
    public <S extends T> S saveAndFlush(S entity) {
        return repository.saveAndFlush(entity);
    }
}

The above code is the commonly used CURD implementation code of BaseService. Most of us directly call the methods provided by Repository. It should be noted that when inheriting BaseServiceImpl, you need to pass your own Repository, as shown in the following example code:

Copy code

@Service
public class UserServiceImpl extends BaseServiceImpl<User, Long, UserRepository> implements UserService {
    public UserServiceImpl(UserRepository repository) {
        super(repository);
    }
    .....
}

Practical thinking only provides a common implementation idea, and you can also expand and expand it according to the actual situation.