Spring Data ES阅读记录

1. 学会使用Spring Data提供的仓库Repositories

Repositories是整个Spring Data大家族的核心概念。几乎所有基础级别的Repositories都是由它衍生出来的。本文基于3.1.0.RC2文档学习总结而来。

1.1 核心概念

 Spring Data中的核心仓库是Repository,仓库可以将域类以及域类的ID类型作为类型参数进行管理,这个接口主要用作标记接口,捕获要使用的类型,并帮助发现扩展此接口的接口(说那么复杂,就我个人里接下来,就2点,第一,一个继承此接口的都是Repository,第二,但凡继承这个接口的,那都能自动被Spring识别为一个Bean)。比如CrudRepository就是继承自Repository接口,它为正在管理的实体类提供了复杂的CRUD功能,这里的“管理的实体类”就是上述的“域类以及域类的ID”,下面是CrudRepository中的一些方法:

@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
    // 保存它所管理的实体类T类型的对象,然后泛型后面接的是实体类的注解类型,这点在后面会有体现
	<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();
}

所以在自定义实现Repository接口时会写上泛型,格式为<管理的实体类的类型,实体类ID的类型>,这就是“仓库可以将域类以及域类的ID类型作为类型参数进行管理”这句话的体现。

 除此以外,Spring Data这个大家族为一些主流的持久化技术提供了特定的仓库Repository,比如为MySQL提供了JpaRepository,为MongoDB提供了MongoRepository等,这些接口除了具备相当通用的持久性技术(如CrudRepository)之外,还扩展了CrudRepository功能。在Repository接口之上有一个额外的PagingAndSortingRepository接口,它添加了额外的方法来简化对实体类的分页访问,里面就两个方法:

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

如果有一个User多个实例进行分页,想要获取第二页的内容(每页显示20条数据),那可以这么干:

PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(new PageRequest(1, 20));

所以对于Data家族的仓库的继承形态大致上如下:
Spring Data ES阅读记录_第1张图片

除了上述的查询方法,还可以派生出计数的功能:

interface UserRepository extends CrudRepository<User, Long> {
    long countByLastname(String lastname);
    long deleteByLastname(String lastname);
    List<User> removeByLastname(String lastname);
}

1.2 查询方法(其实翻译为仓库的使用或许更好)

 标准的CRUD功能仓库通常对底层数据库进行查询,使用Spring Data,声明这些查询分为四个步骤:

1.声明一个接口继承Repository或者Repository的子类,并用泛型标注它管理的类的类型和类的ID的类型,比如:

interface PersonRepository extends Repository<Person, Long> {}

2.在第一步的接口中定义抽象方法,比如:

interface PersonRepository extends Repository<Person, Long> {
  List<Person> findByLastname(String lastname);
}

3.使用JavaConfig或XML配置,让Spring为我们创建的接口创建代理实例,比如:

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories
class Config {}

【注意】JavaConfig没有显式的配置包,因为默认情况下使用带注释的类的包,如果想要定制包扫描,就需要使用特定持久化层仓库中的basePackages配置扫描的目录(可以是多个路径),比如@EnableJpaRepositories(basePackages = "com.glodon.repositories")

4.注入第三步中Spring为我们创建的接口实例对象并使用,比如:

class SomeClient {

  private final PersonRepository repository;

  SomeClient(PersonRepository repository) {
    this.repository = repository;
  }

  void doSomething() {
    List<Person> persons = repository.findByLastname("Matthews");
  }
}


// 下面的方式或许更常见
@Autowired
BookRepository bookRepository;

@RequestMapping(value = "/add_index", method = RequestMethod.POST)
public ResponseEntity<String> indexDoc(@RequestBody Book book) {
    bookRepository.save(book);
    return new ResponseEntity<>("save executed!", HttpStatus.OK);
}

【注意】

第4步中提及,Spring会为接口创建实例化对象,是隐式的创建,只要继承了Data自带的某个仓库接口XXXRepository,那几乎不用再去管具体实现,因为这些接口Data内部是有这些接口的默认实现的,就比如在自定义接口继承ElasticsearchRepository时,我们只在里面写了一些抽象方法,具体实现其实是在SimpleElasticsearchRepository类中实现的。所以如果说不继承Data中任何仓库Repository,自己写了接口用注解进行标注,那这里的标注应该标在具体实现类上(你需要自己写接口中的实现方法,这简直和MVC中的Server层中的接口和ServiceImpl一模一样,也好理解)。

1.3 定义仓库接口

 就像上面所说,自定义仓库接口必须实现Repository接口或者它的子接口,比如想使用Data为我们提供的
CRUD功能,那就可以直接实现CrudRepository子接口,然后为这个接口加上格式为<管理的实体类的类型,实体类ID的类型>的泛型。

1.3.1 定义仓库接口

 由于Data为我们提供了一系列核心仓库Repository的实现接口,有CrudRepositoryPagingAndSortingRepository,它们为继承它们的接口提供了一套完备的方法去操作所管理的实体类,我们自定义的仓库接口可以去实现RepositoryCrudRepository以及PagingAndSortingRepository中的任意一个接口(或者去实现具体持久化层的Data接口,比如Data ES中提供的ElasticsearchCrudRepository或者ElasticsearchRepository),当然,我们也可以不依赖Data提供的上述仓库接口,我就想自己去写仓库接口,然后自己写这个仓库的实现,当然也可以,但是接口的实现类必须使用注解@RepositoryDefinition
进行标注。如果实现Data提供的接口,但需压对已有的方法进行一些定制,那只需要将里面方法复制到自定义的仓库中进行定制即可。下面就是一个自定义的方法的具体实现:

// 自己定制已有方法
@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends Repository<T, ID> {

  Optional<T> findById(ID id);

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

interface UserRepository extends MyBaseRepository<User, Long> {
  User findByEmailAddress(EmailAddress emailAddress);
}

中间的仓库接口(这里的中间仓库接口是指Data的基层接口Repository到具体实现或自定义的接口之间所有的接口)都使用了@NoRepositoryBean注解,一般用作父类的repository(但不包括基层接口Repository),有这个注解,Spring Data不会在运行时实例化有这个注解的仓库接口repository。

1.3.2 仓库方法的空处理

 从Spring Data 2.0开始,仓库的CRUD方法就可以使用Java 8中的Optional表示可能为空的对象,除此之外,Spring Data还支持在查询方法使用以下的包装类包装返回结果:

  • com.google.common.base.Optional
  • scala.Option
  • io.vavr.control.Option
  • javaslang.control.Option(不推荐使用);

当然,我们也可以不使用上述的包装类,如果对象为空我们直接返回一个null,仓库方法返回集合、可选集合、包装类以及流的方式确保了仓库方法的返回值永远不会是null,而是返回相应的空表示,具体参看仓库的返回值类型。

【可空注解】

 可以使用Spring Framework的可空性注释来表示仓库方法的可空性约束。它们在运行时提供了一种工具友好的方法和选择性的null检查,如下所示:

  • @NonNullApi:在包级别上使用,以声明参数和返回值的默认行为是不接受或生成null值的;
  • @NonNull:用于参数或返回值上,表示该参数或者返回值不能为null(在已经在包级别上有@NonNullApi注解的类上中就不需要再用这个注解了);
  • @Nullable:用于参数或返回值上,表示参数或返回值可以为null

Spring注解是带有JSR 305的元注解,JSR 305元注解让类似于IDEA、Eclipse以及Kotlin这类工具提供商提了通用的空安全支持,无须对Spring注解具备硬编码支持。为了使查询方法可以在运行时进行空检测,必须使用Sprinpackage-info.java中的@NonNullApi在包级别上激活非空性检测,比如下面在package-info.java中声明不可为空性:

// com.acme包以及它的子包中的方法参数以及返回值都不允许为空!!
@org.springframework.lang.NonNullApi
package com.acme;

【注意】包级别的注解不能像普通java文件那样去创建,因为IDEA中命令是不允许创建的类名含有-字符,所以需要在外部创建好了package-info.java文件然后托进来,再加上包级别的注解。

一旦像上述存在非空默认,就会在调用仓库中查询方时(运行时)验证可空性约束,如果一个查询方法的返回结果违反了定义的非空限制,就会抛异常(返回值为空抛出的异常为org.springframework.dao.EmptyResultDataAccessException,参数为空抛出来的异常为IllegalArgumentException)。上述注解是对整个包级别而言都不能为空,但如果想选择性的允许个别返回值为空,那可以在个别方法上使用@Nullable注解。下面是具体的案例使用:

package com.acme;

import org.springframework.lang.Nullable;

interface UserRepository extends Repository<User, Long> {

  User getByEmailAddress(EmailAddress emailAddress);

  // 这个方法的返回值和参数允许为空,其他方法不允许为空(包注解已经整体加了@NonNullApi注解)
  @Nullable
  User findByEmailAddress(@Nullable EmailAddress emailAdress);

  Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress);
}

如果像上面使用Optional来包装返回值,如果没有响应的返回值,那就返回Optional.empty()

1.3.3 使用具有多个Spring Data模块的仓库

 因为Data是一个大家族,除了Data ES还有其他的模块(比如Data Couchbase、Data JPA等等),在实际开发中可能不仅仅只用到Data中的Data ES模块,还可能有其他的模块,此时不同持久化层的仓库区分开,当Data在类路径上检测到多个仓库时(即在@EnableElasticsearchRepositories注解上使用basePackages标明的包路径),Spring Data将进入严格仓库配置模式,严格配置模式使用仓库或域类(即仓库所管理的实体类)的详细信息来确定仓库定义的Spring Data模块绑定:

  1. 如果仓库定义扩展了特定于模块的仓库接口,那么它就是特定Spring Data模块的有效候选者(比如自定义的仓库接口继承了ElasticsearchRepository,那Data就会直接将这个自定义接口绑定到Data ES模块上);
  2. 如果使用特定于模块的类型注释对域类(即接口所管理的类)进行注释,则它是特定Spring Data模块的有效候选者。Spring Data模块接受第三方注释(例如JPA的@Entity)或提供自己的注释(例如Spring Data MongoDB和Spring Data Elasticsearch的@Document)。简单的说就仓库接口的域类(所管理的类)上使用的特定的注解,Data就会根据域类的注解将它的仓库接口帮到特定的Data模块上。

下面是使用多模块的接口的例子(以JPA为例),说明上述情况中的第一种情况:

interface MyRepository extends JpaRepository<User, Long> { }

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {}

interface UserRepository extends MyBaseRepository<User, Long> {}

MyRepositoryUserRepository在层次结构上都继承了JpaRepository,那很明显Spring Data JPA就是有效的绑定模块。

下面是使用通用接口的例子,说明上述绑定情况的第二种:

interface AmbiguousRepository extends Repository<User, Long> {}

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {}

interface AmbiguousUserRepository extends MyBaseRepository<User, Long> {}

AmbiguousRepositoryAmbiguousUserRepository只是继承了通用的RepositoryCrudRepository(不像上述第一个例子继承了特定模块的接口),如果项目中只有Data家族中的一个模块,那Data很轻松的就可以将自定义的仓库绑定到那个Data模块上,因为只有这一个模块,但是如果项目中有多个Data模块,那Spring Data就会很懵逼啊,因为上述和两个接口是通用仓库接口,只要是Data家族中的成员都继承了这些通用接口。看下面的例子:

interface PersonRepository extends Repository<Person, Long> {}

@Entity
class Person {}

interface UserRepository extends Repository<User, Long> {}

@Document
class User {}

PersonRepository继承的虽然是通用仓库接口Repository,无法确定绑定的Data模块,但是它管理的是Person类,Person类上使用的注解@Entity是JPA模块的注解,所以Data也很聪明,会将PersonRepository绑定到Data JPA模块上,同样的UserRepository管理的实体类User使用了@Document注解,可以推断出是Data MongoDB或者Data ES模块,具体看注解内部的属性判断。

【问题】这里我其实有个疑问,如果域类上有多个类型的注解,那这个仓库接口怎么确定绑到哪个模块上呢?

关于上面的疑问,Data也给出了域类混合注解使用情况,这样确实是不被允许的,比如下面的:

interface JpaPersonRepository extends Repository<Person, Long> {}

interface MongoDBPersonRepository extends Repository<Person, Long> {}

@Entity
@Document
class Person {}

上面的代码片定义了两个仓库接口JpaPersonRepositoryMongoDBPersonRepository,域类使用了@Entity@Document注解,分别是JPA和MongoDB的注解,Data将不能将二者区分开,导致未定义的错误。

关于上述提出的疑问,官方给出和解答是:

Using multiple persistence technology-specific annotations on the same domain type is possible and enables reuse of domain types across multiple persistence technologies. However, Spring Data can then no longer determine a unique module with which to bind the repository.

最新的解决方案是将不同持久化仓库接口定义到不同的包中,然后再配置文件中通过不同的@EnableXXXRepositories注解标出各个持久化仓库的路径,这样就比较明确了,比如:

@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
interface Configuration { }

1.4 定义查询方法

repository定义了2种按名字派生出方法的策略:

  • 直接从方法名直接解析查询;
  • 手动定义查询;

1.4.1 创建查询

 Spring Data reppository支持直接按名字解析,但是起的方法名要按照一定的规则来取,解析的时候会剥离以下的套词,然后对剩下的部分进行解析,查询的套词如下:find…By,read…By, query…By, count…Byget…By
反正不管怎样,By后面就是实际查询条件的开始,一般都是基于实体的属性作条件,条件之间使用And或者Or来连接,比如findBookByIdAndNamefindBookByNameOrAuth,除此之外,还可以使用Distinct设置不同的查询方法标识,下面是官方给出具体查询案例:

interface PersonRepository extends Repository<User, Long> {

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // Enables the distinct flag for the query
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

 具体的解析结果和持久化工具有关,但查询这一块也有一些通用的注意点:

  • 遍历属性和级联操作通常都可以组成一个表达式,可以使用AndOr来组成表达式,更深层次的还可以使用BetweenLessThanGreaterThanLike这样的词组成表达式并获得解析支持,当然具体要看持久化层的支持;
  • 方法名解析器还支持IgnoreCase关键字(这是对实体类的某些属性或者是某类属性而言,通常是指String类属性),比如findByLastnameIgnoreCase(…)findByLastnameAndFirstnameAllIgnoreCase(…),和上述的一样,具体还是要看持久化层的支持;
  • 除上述的功能,还可以追加OrderByXxx并提供AscDesc进行静态排序,关于静态排序,参看特殊参数处理章节;

1.4.2 属性表达式

 除了上一小节中简单使用单属性,还可以适用复合属性(即实体类的一个属性是另一个实体,比如Person有一个带ZipCodeAddress),那么正常的方法名就写成这样:

List<Person> findByAddressZipCode(ZipCode zipCode);

 那么解析算法首先将整个部分(AddressZipCode)解释为属性,并检查域类中是否具有该名称的属性(未大写),如果解析成功就认为Person中确有这个属性字段,如果没有(即没有解析到AddressZipCode这个属性),解析算法将来自右侧的驼峰案例部分的源分成头部和尾部并试图找到相应的属性(这个案例中就是分隔为AddressZipCode,注意这里是分为头部和尾部2个部分!所以不存在AddressZip以及Code这样的分隔情况,另外是从右侧开始分隔,所以不会出现AddressZipCode这样的情况,从右侧向左侧移,发现驼峰搞一个出来,剩下的就是另一个了),如果使用头部(Code)在Person类中找到了对应属性,那继续采取尾部并继续从那里建造分隔,按照刚刚描述的方式将尾巴分开。上述是第一次分隔,如果第一次的分割不匹配,算法会将分割点移动到左侧(AddressZipCode)并继续上述的过程。

 上面的场景说明了Data解析算法的流程,虽然这个解析算法可以应对大多数的场景,但是如果Person类中刚好还有一个addressZip属性呢,那此时这个解析算法解析的第一轮时候就会匹配,然后失败(第一次分隔就可以匹配上addressZip,但可能没有code这个属性)。为了解决上述歧义的问题,可以在方法名中使用_手动定义遍历点,此时的方法名应该为:

// 认为指定分隔为Address和ZipCode
List<Person> findByAddress_ZipCode(ZipCode zipCode);

1.4.3 特殊参数的处理

 之前已经有部分参数的设置,除此之外,ES Data还可识别某些特定类型参数,如使用PageableSort动态进行分页或排序,如果需要分页,那直接在对应的方法中丢一个Pageable对象进去,排序也是如此,比如:

Page<User> findByLastname(String lastname, Pageable pageable);

Slice<User> findByLastname(String lastname, Pageable pageable);

List<User> findByLastname(String lastname, Sort sort);

List<User> findByLastname(String lastname, Pageable pageable);

1.4.4 限制查询结果

 查询方法的结果可以通过使用firsttop关键字来限制,这些关键字可以互换使用。可选的数值可以附加到topfirst后面,以指定要返回的结果的前几个数据,比如下面的:

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

这些限制表达式也支持Distinct使用,此外,对于将结果集限制为一个实例的查询,支持使用Optional关键字将结果进行包装。

1.4.5 流式的查询结果(仅jdk8+)

 可以使用Java 8的Stream 作为返回类型以递增方式处理查询方法的结果,而不是将查询结果包装在Stream中,数据存储特定的方法用于执行流式处理,如以下例所示:

@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);

这里需要注意的是Stream可能包装着特定持久化层的资源,所以用完必须关闭,可以手动调用Stream.close(),或者使用try-catch块:

try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach();
}

【注意】不是所有的Spring Data模块都支持结果流式化的。

1.4.6 异步查询结果

 Repository可以使用Spring异步方法的执行能力进行异步查询,这意味着该方法在调用时立即返回,而实际的查询执行命令以task的形式提交给Spring的TaskExecutor任务中。异步式查询不能和响应式查询(即不管怎样,一定要返回一个结果)混用,下面是一些异步查询的案例:

// 返回结果为java.util.concurrent.Future类型
@Async
Future<User> findByFirstname(String firstname);

// 返回结果为java.util.concurrent.CompletableFuture类型
@Async
CompletableFuture<User> findOneByFirstname(String firstname);

// 返回结果为org.springframework.util.concurrent.ListenableFuture类型
@Async
ListenableFuture<User> findOneByLastname(String lastname);

1.5 配置Repository实例

 可以通过XML和JavaConfig配置,后者是较为推荐的配置方式,使用Spring Data的Repository配置简单如下:

@Configuration
// 扫描基类包,实际根据使用的Repositories进行更换,比如ES使用的是@EnableElasticsearchRepositories
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {

  @Bean
  EntityManagerFactory entityManagerFactory() {
    // …
  }
}

// ES的配置是
@Configuration
@EnableElasticsearchRepositories(basePackages = "com.glodon.repositories")
public interface ESConfig {
    // ...
}

1.6 继承Spring Data的Repository接口自定义自用的Repository接口

 当查询的方法需要不同的行为或无法通过自带查询派生实现时,就需要提供一个自定义实现。Spring Data repositories允许我们自定义repository,并将其与通用CRUD抽象和查询方法功能集成。我们可以自定义接口,完全不依赖Spring Data,比如:

// 自定义的接口
interface CustomizedUserRepository {
  void someCustomMethod(User user);
}

// 实现自定义接口中的具体方法
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
  public void someCustomMethod(User user) {
    // Your custom implementation
  }
}

除此之外,还可以自定义接口,并实现原来Data自带的Repository接口和自定义的接口,这样可以不断加强我们的接口能力,比如:

// CrudRepository是Data自带的
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {
  // Declare query methods here
}

然后可以不断自定义接口,添加到最后的接口中,这样最终的接口就会有丰富的能力:

// 接口1
interface HumanRepository {
  void someHumanMethod(User user);
}

// 实现1
class HumanRepositoryImpl implements HumanRepository {

  public void someHumanMethod(User user) {
    // Your custom implementation
  }
}

// 接口2
interface ContactRepository {

  void someContactMethod(User user);

  User anotherContactMethod(User user);
}

// 实现2
class ContactRepositoryImpl implements ContactRepository {

  public void someContactMethod(User user) {
    // Your custom implementation
  }

  public User anotherContactMethod(User user) {
    // Your custom implementation
  }
}

// 最终的使用的Repository接口
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {
  // Declare query methods here
}

【注意】 我们自定义的接口优先级是高于Data自带的基本级别的Repository,这样可以让我们可以覆盖掉原来自带的Repository里面的某些方法。

2. Data ES 的使用

 目前官方给出的版本搭配为:

spring data elasticsearch elasticsearch
3.1.x 6.2.2
3.0.x 5.5.0
2.1.x 2.4.0
2.0.x 2.2.0
1.3.x 1.5.2

2.1 配置repositories

 基于JavaConfig的配置方式repositories

@Configuration
@EnableElasticsearchRepositories(basePackages = "org/springframework/data/elasticsearch/repositories")
static class Config {
    @Bean
    public ElasticsearchOperations elasticsearchTemplate() {
        return new ElasticsearchTemplate(nodeBuilder().local(true).node().client());
    }
}

上述配置了内嵌了ES Server,它是被ES模板ElasticsearchTemplate使用的,使用@EnableElasticsearchRepositories注解可以激活Spring Data ES中的Repositories,如果这里不指定基类包,那么它将会扫描该配置文件所在目录。

 除此之外,还可以使用CDI设置Spring Data es的repositories,比如下面的:

class ElasticsearchTemplateProducer {

    @Produces
    @ApplicationScoped
    public ElasticsearchOperations createElasticsearchTemplate() {
        return new ElasticsearchTemplate(nodeBuilder().local(true).node().client());
    }
}

class ProductService {

    private ProductRepository repository;

    public Page<Product> findAvailableBookByName(String name, Pageable pageable) {
        return repository.findByAvailableTrueAndNameStartingWith(name, pageable);
    }

    @Inject
    public void setRepository(ProductRepository repository) {
        this.repository = repository;
    }
}

2.2 查询方法

 查询策略,Data ES支持所有的基本查询功能,如String、Abstract、Criteria,从方法名派生的查询并不总是高效的,AndOr可能导致不可读的方法名称,这种场景下可以尝试一下@Query注解。查询时基本和Data的查询大差不差,比如:

public interface BookRepository extends Repository<Book, String>
{
    List<Book> findByNameAndPrice(String name, Integer price);
}

上面的findByNameAndPrice这个方法将会被Data ES翻译成下面的json查询(bool查询):

{ "bool" :
    { "must" :
        [
            { "field" : {"name" : "?"} },
            { "field" : {"price" : "?"} }
        ]
    }
}

使用@Query注解的,可以写成这样:

public interface BookRepository extends ElasticsearchRepository<Book, String> {
    @Query("{"bool" : {"must" : {"field" : {"name" : "?0"}}}}")
    Page<Book> findByName(String name,Pageable pageable);
}

其中方法中ES可以支持的关键字有:

Data ES支持方法名中解析的关键字
关键字 小栗子 ES查询语句

And

findByNameAndPrice

{"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}

Or

findByNameOrPrice

{"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}

Is

findByName

{"bool" : {"must" : {"field" : {"name" : "?"}}}}

Not

findByNameNot

{"bool" : {"must_not" : {"field" : {"name" : "?"}}}}

Between

findByPriceBetween

{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}

LessThanEqual

findByPriceLessThan

{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}

GreaterThanEqual

findByPriceGreaterThan

{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}

Before

findByPriceBefore

{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}

After

findByPriceAfter

{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}

Like

findByNameLike

{"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}}

StartingWith

findByNameStartingWith

{"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}}

EndingWith

findByNameEndingWith

{"bool" : {"must" : {"field" : {"name" : {"query" : "*?","analyze_wildcard" : true}}}}}

Contains/Containing

findByNameContaining

{"bool" : {"must" : {"field" : {"name" : {"query" : "?","analyze_wildcard" : true}}}}}

In

findByNameIn(Collectionnames)

{"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}}

NotIn

findByNameNotIn(Collectionnames)

{"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}}

Near

findByStoreNear

Not Supported Yet !

True

findByAvailableTrue

{"bool" : {"must" : {"field" : {"available" : true}}}}

False

findByAvailableFalse

{"bool" : {"must" : {"field" : {"available" : false}}}}

OrderBy

findByAvailableTrueOrderByNameDesc

{"sort" : [{ "name" : {"order" : "desc"} }],"bool" : {"must" : {"field" : {"available" : true}}}}

3. 复杂ES操作的支持

 无法通过repository接口直接访问ES进行的操作,建议作为自定义repository中一部分自己去实现。

3.1 过滤

 过滤器可以加快查询速度:

private ElasticsearchTemplate elasticsearchTemplate;

SearchQuery searchQuery = new NativeSearchQueryBuilder()
    .withQuery(matchAllQuery())
    .withFilter(boolFilter().must(termFilter("id", documentId)))
    .build();

Page<SampleEntity> sampleEntities =
    elasticsearchTemplate.queryForPage(searchQuery,SampleEntity.class);

3.2 为大数据量的结果集提供浏览和滚动的功能

 Elasticsearch可以为块中大数据量的结果集提供扫描和滚动功能,ElasticsearchTemplate也有扫描和滚动方法,比如下面的:

SearchQuery searchQuery = new NativeSearchQueryBuilder()
    .withQuery(matchAllQuery())
    .withIndices("test-index")
    .withTypes("test-type")
    .withPageable(new PageRequest(0,1))
    .build();
String scrollId = elasticsearchTemplate.scan(searchQuery,1000,false);
List<SampleEntity> sampleEntities = new ArrayList<SampleEntity>();
boolean hasRecords = true;
while (hasRecords){
    Page<SampleEntity> page = elasticsearchTemplate.scroll(scrollId, 5000L , new ResultsMapper<SampleEntity>()
    {
        @Override
        public Page<SampleEntity> mapResults(SearchResponse response) {
            List<SampleEntity> chunk = new ArrayList<SampleEntity>();
            for(SearchHit searchHit : response.getHits()){
                if(response.getHits().getHits().length <= 0) {
                    return null;
                }
                SampleEntity user = new SampleEntity();
                user.setId(searchHit.getId());
                user.setMessage((String)searchHit.getSource().get("message"));
                chunk.add(user);
            }
            return new PageImpl<SampleEntity>(chunk);
        }
    });
    if(page != null) {
        sampleEntities.addAll(page.getContent());
        hasRecords = page.hasNextPage();
    }
    else{
        hasRecords = false;
    }
    }
}

你可能感兴趣的:(#,ES,Elasticsearch)