Spring Data JPA 查询方法的命名语法与参数

3 Spring Data JPA 查询方法的命名语法与参数

在⼯作中,你是否经常为⽅法名的语义、命名规范⽽发愁?是否要为不同的查询条件写各种的 SQL 语句?是否为同⼀个实体的查询,写⼀个超级通⽤的查询⽅法或者 SQL?如果其他开发同事不查看你写的 SQL 语句,⽽直接看⽅法名的话,却不知道你想查什么⽽郁闷?Spring Data JPA 的 Defining Query Methods(DQM)通过⽅法名和参数,可以很好地解决上⾯的问题,也能让我们的⽅法名的语义更加清晰,开发效率也会提升很多。DQM 语法共有 2 种,可以实现上⾯的那些问题,具体如下:

  • ⼀种是直接通过⽅法名就可以实现;(本节重点)

  • 另⼀种是 @Query ⼿动在⽅法上定义;

下⾯将从 6 个⽅⾯来详细讲解 Defining Query Methods。先来分析⼀下“定义查询⽅法的配置和使⽤⽅法”,这个是 Defining Query Methods 中必须要掌握的语法。

3.1 定义查询方法的配置和使用方法

若想要实现 CRUD 的操作,常规做法是写⼀⼤堆 SQL 语句。但在 JPA ⾥⾯,只需要继承 Spring Data Common ⾥⾯的任意 Repository 接⼝或者⼦接⼝,然后直接通过⽅法名就可以实现,例如实现 CrudRepository 接口:

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

这个时候就可以直接调⽤ CrudRepository ⾥⾯暴露的所有接⼝⽅法,以及 UserRepository ⾥⾯定义的⽅法,不需要写任何 SQL 语句,也不需要写任何实现⽅法。完成了 Defining Query Methods(DQM)的基本使⽤。

然⽽,有时如果不想暴露 CrudRepository ⾥⾯的所有⽅法,那么可以直接继承我们认为需要暴露的那些⽅法的接⼝。假如 UserRepository 只想暴露 findOne 和 save,除了这两个⽅法之外不允许任何的 User 操作,其做法如下。

我们选择性地暴露 CRUD ⽅法,直接继承Repository(因为这⾥⾯没有任何⽅法),把 CrudRepository ⾥⾯的 save 和 findOne ⽅法复制到我们⾃⼰的 MyBaseRepository 接⼝即可,代码如下:

@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); 
}

注意要加上 @NoRepositoryBean 注解。

这样在 Service 层就只有 findOne、save、findByEmailAddress 这 3 个⽅法可以调⽤,不会有更多⽅法了,我们可以对 SimpleJpaRepository ⾥⾯任意已经实现的⽅法做选择性暴露。

综上所述,得出以下 2 点结论:

  1. MyRepository Extends Repository 接⼝可以实现 Defining Query Methods 的功能;
  2. 继承其他 Repository 的⼦接⼝,或者⾃定义⼦接⼝,可以选择性地暴露 SimpleJpaRepository ⾥⾯已经实现的基础公⽤⽅法。

3.2 方法的查询策略设置

在平时的⼯作中,可以通过⽅法名,或者定义⽅法名上⾯添加 @Query 注解两种⽅式来实现 CRUD 的⽬的,⽽ Spring 给我们提供了两种切换⽅式。接下来我们就讲讲“⽅法的查询策略设置”。

⽬前在实际⽣产中还没有遇到要修改默认策略的情况,但我们必须要知道有这样的配置⽅法,做到⼼中有数,这样我们才能知道为什么⽅法名可以,@Query 也可以。通过@EnableJpaRepositories 注解来配置⽅法的查询策略,详细配置⽅法如下:

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

其中,QueryLookupStrategy.Key 的值共 3 个,具体如下:

  • Create:直接根据⽅法名进⾏创建,规则是根据⽅法名称的构造进⾏尝试,⼀般的⽅法是从⽅法名中删除给定的⼀组已知前缀,并解析该⽅法的其余部分。如果⽅法名不符合规则,启动的时候会报异常,这种情况可以理解为,即使配置了 @Query 也是没有⽤的。
  • USE_DECLARED_QUERY:声明⽅式创建,启动的时候会尝试找到⼀个声明的查询,如果没有找到将抛出⼀个异常,可以理解为必须配置 @Query。
  • CREATE_IF_NOT_FOUND:这个是默认的,除⾮有特殊需求,可以理解为这是以上2 种⽅式的兼容版。先⽤声明⽅式(@Query)进⾏查找,如果没有找到与⽅法相匹配的查询,那⽤ Create 的⽅法名创建规则创建⼀个查询;这两者都不满⾜的情况下,启动就会报错。

3.3 Defining Query Method(DQM)语法

Defining Query Method 语法是:带查询功能的⽅法名由查询策略(关键字)+ 查询字段 + ⼀些限制性条件组成,具有语义清晰、功能完整的特性,我们实际⼯作中 80% 的 API 查询都可以简单实现。

我们来看⼀个复杂点的例⼦,这是⼀个 and 条件更多、distinct or 排序、忽略⼤⼩写的例⼦。下⾯代码定义了 PersonRepository,我们可以在 service 层直接使⽤,如下所示:

interface PersonRepository extends Repository<User, Long> {
    
    // and 的查询关系
    List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);

    // 包含 distinct 去重,or 的 sql 语法
    List<User> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);

    // 根据 lastname 字段查询忽略⼤⼩写
    List<User> findByLastnameIgnoreCase(String lastname);

    // 根据 lastname 和 firstname 查询 equal 并且忽略⼤⼩写
    List<User> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

    // 对查询结果根据 lastname 排序,正序
    List<User> findByLastnameOrderByFirstnameAsc(String lastname);

    // 对查询结果根据 lastname 排序,倒序
    List<User> findByLastnameOrderByFirstnameDesc(String lastname);
}

下⾯表格是⼀个我们在上⾯ DQM ⽅法语法⾥常⽤的关键字列表,⽅便你快速查阅,并满⾜在实际代码中更加复杂的场景:

关键字 Keyword案例 SampleJPQL 表达式
DistinctfindDistinctByLastnameAndFirstnameselect distinct … where x.lastname = ?1 and x.firstname = ?2
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
Is, EqualsfindByFirstname,findByFirstnameIs,findByFirstnameEquals… where x.firstname = ?1
BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2
LessThanfindByAgeLessThan… where x.age < ?1
LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1
AfterfindByStartDateAfter… where x.startDate > ?1
BeforefindByStartDateBefore… where x.startDate < ?1
IsNull, NullfindByAge(Is)Null… where x.age is null
IsNotNull, NotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
StartingWithfindByFirstnameStartingWith… where x.firstname like ?1 (parameter bound with appended %)
EndingWithfindByFirstnameEndingWith… where x.firstname like ?1 (parameter bound with prepended %)
ContainingfindByFirstnameContaining… where x.firstname like ?1 (parameter bound wrapped in %)
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection<Age> ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection<Age> ages)… where x.age not in ?1
TruefindByActiveTrue()… where x.active = true
FalsefindByActiveFalse()… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstname) = UPPER(?1)

综上,总结 3 点经验:

  • ⽅法名的表达式通常是实体属性连接运算符的组合,如 And、or、Between、LessThan、GreaterThan、Like 等属性连接运算表达式,不同的数据库(NoSQL、MySQL)可能产⽣的效果不⼀样,如果遇到问题,我们可以打开 SQL ⽇志观察。

  • IgnoreCase 可以针对单个属性(如 findByLastnameIgnoreCase(…)),也可以针对查询条件⾥⾯所有的实体属性忽略⼤⼩写(所有属性必须在 String 情况下,如findByLastnameAndFirstnameAllIgnoreCase(…))。

  • OrderBy 可以在某些属性的排序上提供⽅向(Asc 或 Desc),称为静态排序,也可以通过⼀个⽅便的参数 Sort 实现指定字段的动态排序的查询⽅法(如repository.findAll(Sort.by(Sort.Direction.ASC, “myField”)))。

我们看到上⾯的表格虽然⼤多是 find 开头的⽅法,除此之外,JPA 还⽀持 read、get、query、stream、count、exists、delete、remove 等前缀,如字⾯意思⼀样。我们来看看 count、delete、remove 的例⼦,其他前缀可以举⼀反三。实例代码如下:

interface UserRepository extends CrudRepository<User, Long> {
    //查询总数
    long countByLastname(String lastname);

    //根据⼀个字段进⾏删除操作,并返回删除⾏数
    long deleteByLastname(String lastname);

    //根据Lastname删除⼀堆User,并返回删除的User
    List<User> removeByLastname(String lastname);
}

有的时候随着版本的更新,也会有更多的语法⽀持,或者不同的版本语法可能也不⼀样,我们通过源码来看⼀下上⾯说的⼏种语法。感兴趣的同学可以到类 org.springframework.data.repository.query.parser.PartTree 查看相关源码的逻辑和处理⽅法,关键源码如下:

在这里插入图片描述

再看一下两个子类:
在这里插入图片描述

在这里插入图片描述

根据源码我们也可以分析出来,query method 包含其他的表达式,⽐如 find、count、delete、exist 等关键字在 by 之前通过正则表达式匹配。

我们可以查看 org.springframework.data.repository.query.parser.Part.Type 源码,看该枚举帮我们定义好了什么关键字

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

看源码就可以知道框架⽀持了哪些逻辑关键字,⽐如 NotIn、Like、In、Exists 等,有的时候⽐查⽂档和任何⼈写的博客都准确、还快。好了,上⾯介绍了⽅⾯名的基本表达⽅式,希望你可以在⼯作中灵活运⽤,举⼀反三。接下来我们讲讲特定类型的参数:Sort 排序和 Pageable 分⻚,这是分⻚和排序必备技能。

3.4 特定类型的参数:Sort 和 Pageable

Spring Data JPA 为了⽅便我们排序和分⻚,⽀持了两个特殊类型的参数:Sort 和 Pageable。

  • Sort ⾥⾯决定了我们哪些字段的排序⽅向(ASC 正序、DESC 倒序)。

  • Pageable 在查询的时候可以实现分⻚效果和动态排序双重效果,

我们看下 Pageable 的 Structure:

在这里插入图片描述

我们发现 Pageable 是⼀个接⼝,⾥⾯有常⻅的分⻚⽅法排序、当前⻚、下⼀⾏、当前指针、⼀共多少⻚、⻚码、pageSize 等。

在查询⽅法中如何使⽤ Pageable 和 Sort 呢?下⾯代码定义了根据 Lastname 查询 User 的分⻚和排序的实例,此段代码是在 UserRepository 接⼝⾥⾯定义的⽅法:

//根据分⻚参数查询User,返回⼀个带分⻚结果的Page(下⼀课时详解)对象(⽅法⼀)
Page<User> findByLastname(String lastname, Pageable pageable);

//我们根据分⻚参数返回⼀个Slice的user结果(⽅法⼆)
Slice<User> findByLastname(String lastname, Pageable pageable);

//根据排序结果返回⼀个List(⽅法三)
List<User> findByLastname(String lastname, Sort sort);

//根据分⻚参数返回⼀个List对象(⽅法四)
List<User> findByLastname(String lastname, Pageable pageable);
  1. ⽅法⼀:允许将 org.springframework.data.domain.Pageable 实例传递给查询⽅法,将分⻚参数添加到静态定义的查询中,通过 Page 返回的结果得知可⽤的元素和⻚⾯的总数。这种分⻚查询⽅法可能是昂贵的(会默认执⾏⼀条 count 的 SQL 语句),所以⽤的时候要考虑⼀下使⽤场景。
  2. ⽅法⼆:返回结果是 Slice,因为只知道是否有下⼀个 Slice 可⽤,⽽不知道 count,所以当查询较⼤的结果集时,只知道数据是⾜够的,也就是说⽤在业务场景中时不⽤关⼼⼀共有多少⻚。
  3. ⽅法三:如果只需要排序,需在 org.springframework.data.domain.Sort 参数中添加⼀个参数,正如上⾯看到的,只需返回⼀个 List 也是有可能的。
  4. ⽅法四:排序选项也通过 Pageable 实例处理,在这种情况下,Page 将不会创建构建实际实例所需的附加元数据(即不需要计算和查询分⻚相关数据),⽽仅仅⽤来做限制查询给定范围的实体。

那么如何使⽤呢?我们再来看⼀下源码,也就是 Pageable 的实现类,如下图所示:

在这里插入图片描述

由此可知,我们可以通过 PageRequest ⾥⾯提供的⼏个 of 静态⽅法(多态),分别构建⻚码、⻚⾯⼤⼩、排序等,Sort 的源码类似。我们来看下,在使⽤中的写法,如下所示:

// 按照 id 升序查询 user ⾥⾯的第⼀⻚,每⻚⼤⼩是20条;并会返回⼀共有多少⻚的信息
Page<User> page = userRepository.findAll(PageRequest.of(0, 20, Sort.by(Sort.Direction.ASC, "id")));

在实际⼯作中,如果遇到不知道参数怎么传递的情况,可以看⼀下源码,因为 Java 是类型安全的。

3.5 限制查询结果:First 和 Top

有的时候我们想直接查询前⼏条数据,也不需要动态排序,那么就可以简单地在⽅法名字中使⽤ First 和 Top 关键字,来限制返回条数。

我们来看看 userRepository ⾥⾯可以定义的⼀些限制返回结果的使⽤。在查询⽅法上加限制查询结果的关键字 First 和 Top。

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

其中:

  • 查询⽅法在使⽤ First 或 Top 时,数值可以追加到 First 或 Top 后⾯,指定返回最⼤结果的⼤⼩;
  • 如果数字被省略,则假设结果⼤⼩为 1;
  • 限制表达式也⽀持 Distinct 关键字;
  • ⽀持将结果包装到 Optional 中。
  • 如果将 Pageable 作为参数,size 以 Top 和 First 后⾯的数字为准。

3.6 @NonNull、@NonNullApi 和 @Nullable 关键字

从 Spring Data 2.0 开始,JPA 新增了@NonNull @NonNullApi @Nullable,是对 null 的参数和返回结果做的⽀持。

  • @NonNullApi:在包级别⽤于声明参数,以及返回值的默认⾏为是不接受或产⽣空值的。
  • @NonNull:⽤于不能为空的参数或返回值(在 @NonNullApi 适⽤的参数和返回值上不需要)。
  • @Nullable:⽤于可以为空的参数或返回值。

我们可以在 Repository 所在 package 的 package-info.java 类⾥⾯做如下声明:

@org.springframework.lang.NonNullApi
package com.example.jpa.example1;

UserRepository 实现如下:

interface UserRepository extends Repository<User, Long> {
    User getByEmailAddress(String emailAddress); 
}

这个时候当 emailAddress 参数为 null 的时候就会抛 IllegalArgumentException 异常,当返回结果为 null 的时候会抛 EmptyResultDataAccessException 异常。因为我们在 package 的 package-info.java ⾥⾯指定了NonNullApi,所有返回结果和参数不能为 null。

// 添加 @Nullable 注解之后,参数和返回结果这个时候就都会允许为 null 了;
@Nullable
User findByEmailAddress(@Nullable String emailAdress);

// 返回结果允许为 null,参数不允许为 null 的情况
Optional<User> findOptionalByEmailAddress(String emailAddress); 

3.7 思考

我们学习了 Defining Query Methods 的语法和其所表达的命名规范,在实际⼯作中,也可以将⽅法名(⾮常语义化的 respository ⾥⾯所定义⽅法命名规范)的强制约定规范运⽤到 controller 和 service 层,这样全部统⼀后,可以减少很多的沟通成本。

Spring Data Common ⾥⾯的 repository 基类,是否可以应⽤推⼴到 service 层,建⽴⼀个⾃⼰的 baseService?我们来看下⾯的实战例⼦:

  • 自定义 BaseRepository
@NoRepositoryBean
interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
}
  • 定义 BaseService
public interface BaseService<T, ID> {

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

}
  • 定义实现类 BaseServiceImpl
public class BaseServiceImpl<T, ID extends Serializable, R extends BaseRepository<T, ID>> implements BaseService<T, ID> {

    @Autowired
    private R repository;

    protected R getRepository() {
        return repository;
    }

    @Override
    public <S extends T> S save(S entity) {
        return repository.save(entity);
    }
}

然后我们就可以在 BaseServiceImpl 里面实现我们的一些通用操作,例如保存时更新修改人和修改时间。

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值