Spring|Spring Data Elasticsearch的使用

最近在项目开发过程中发现项目接口调用日志表存在一定的问题,为了记录项目中所有的接口调用数据专门用了一个表来存储请求接口的报文信息,一直以来也没出现什么问题,上次我在和外部系统对接时发现,该接口返回的数据比较大,少的时候也有几百Kb,这就导致了日志存储这一点存在问题,这么大的数据使用mysql感觉已经不能满足开发的需要了,所以我就想能不能换一种方式来存储,比如ES或者MongoDB。最终我还是选择了ES,一是项目中已经在使用ES;二是单独搭建一个MongoDB就存储一个表的数据感觉有点浪费。今天就来学习一下使用ES来存储数据,并实现增删改查的功能。
之前自己也使用过使用ES来代替传统的关系型数据库,可以看文章:ES使用遇到的问题。但是因为版本升级,之前的一些API已经是过时了,所以我决定在新版本的基础上重新来学习一下。
一、项目准备
首先说一下本次使用的ES是7.6.0,Spring Boot则是2.4.0,因为不同的版本在使用的过程中还是会有一些差别,这点大家注意一下。Spring Data Elasticsearch文档地址。
按照惯例还是创建一个简单的Spring Boot项目,并引入必要的依赖,比如ES,这里说一下我建议直接使用Spring Data Elasticsearch,项目pom.xml如下:

4.0.0【Spring|Spring Data Elasticsearch的使用】org.springframework.boot spring-boot-starter-parent 2.4.0 com.ypc.spring.data elastic 1.0-SNAPSHOT elastic ES project for Spring Boot1.8 org.springframework.boot spring-boot-starter-data-elasticsearch org.springframework.boot spring-boot-starter-web cn.hutool hutool-all 5.5.2 org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin

接着就是配置文件,主要是配置ES地址、用户名、密码等,这个和之前配置是不一样的,如下:
spring.elasticsearch.rest.uris=localhost:9200 spring.elasticsearch.rest.connection-timeout=6s spring.elasticsearch.rest.read-timeout=10s # spring.elasticsearch.rest.password= # spring.elasticsearch.rest.username=

因为我本地ES没有设置用户名和密码,所以就略去了。
接下我们需要创建我们的数据结构,这个和原来基本是一样的。比如我创建一个UserEntity,如下:
@Data @Document(indexName = "user_entity_index",shards = 1,replicas = 1,createIndex = false) public class UserEntity {@Id private String id; @Field(type = FieldType.Keyword, store = true) private String userName; @Field(type = FieldType.Keyword, store = true) private String userCode; @Field(type = FieldType.Text, store = true,index = false) private String userAddress; @Field(type = FieldType.Keyword, store = true) private String userMobile; @Field(type = FieldType.Integer, store = true) private Integer userGrade; @Field(type = FieldType.Nested, store = true) private List orderEntityList; @Field(type = FieldType.Keyword, store = true) private String status; @Field(type = FieldType.Integer, store = true,index = false) private Integer userAge; }

@Data public class OrderEntity { @Field(type = FieldType.Keyword, store = true) private String id; @Field(type = FieldType.Keyword, store = true,index = false) private String orderNum; @Field(type = FieldType.Date,format = DateFormat.custom,pattern = "yyyy-MM-dd HH:mm:ss",store = true) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date createTime; @Field(type = FieldType.Date,format = DateFormat.custom,pattern = "yyyy-MM-dd HH:mm:ss",store = true) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date updateTime; @Field(type = FieldType.Keyword, store = true) private String amount; @Field(type = FieldType.Keyword, store = true) private String userId; @Field(type = FieldType.Keyword, store = true) private String mobile; @Field(type = FieldType.Keyword, store = true) private String status; }

@Document注解和使用Spring Data JPA中的@Entity是比较相似的,这个注解定义了索引的名称,分片和备份的数量,还有是否创建索引,我这里选择否,即不自动创建索引,这个下面再说。
@Field则可以对比@Column,这里定义了这个属性的数据类型,是否存储和是否索引。ES支持数据类型还是很多的,对于正常的使用足够了。另外我这里还定义了一个Nested,即一个对象列表,后面我们在看这块内容。对于日期类型,如果是自定义的,必须指定pattern
创建好数据模型之后我们还要做一件事情,就是索引还有就是映射关系,单独只创建索引是不行的,就好比mysql你创建了数据库,你还需要创建表。当然索引和映射关系手动创建也可以,我通过实现ApplicationRunner接口来创建,代码如下:
@Slf4j @Component public class UserRunner implements ApplicationRunner {@Autowired private ElasticsearchOperations elasticsearchOperations; @Override public void run(ApplicationArguments args) throws Exception { IndexCoordinates indexCoordinates = IndexCoordinates.of("user_entity_index"); IndexOperations indexOperations = elasticsearchOperations.indexOps(indexCoordinates); if (!indexOperations.exists()) { // 创建索引 indexOperations.create(); indexOperations.refresh(); // 将映射关系写入到索引,即将数据结构和类型写入到索引 indexOperations.putMapping(UserEntity.class); indexOperations.refresh(); log.info(">>>> 创建索引和映射关系成功 <<<<"); } } }

之前会使用ElasticsearchRestTemplate来创建索引和映射,但是新的版本已经过时了,官方推荐使用ElasticsearchOperations。首先这里创建的索引一定要和@Document注解上的索引保持一致。
ApplicationRunner会在项目启动成功之后运行,所以第一次启动项目之后会自动创建索引并进行映射,然后查看下我们创建的索引以及映射,这里就略过了。接下来我们进行简单的CRUD
二、增删改查
在使用ES进行操作的时候,我们其实可以使用Elasticsearch Repositories也可以使用ElasticsearchOperations接口,当然对于ES的语法不太熟悉且操作比较简单的我建议使用Repositories,因为它在使用上比较简单,如果你又使用过Spring Data JPA的话上手非常的容易。
这里说一下数据结构上的@Id注解,其和传统的数据库主键的作用是一样的,默认的话ES会在后端自动生产一个UUID,当然也可以自己赋值去覆盖。
我们创建一个Repositories接口,如下:
public interface UserRepository extends ElasticsearchRepository {}

其继承了ElasticsearchRepository,当然网上追溯的话可以知道其实ElasticsearchRepository也是继承了CrudRepository接口的。
1、新增 我们创建一个新增的接口:
@PostMapping("/save") public ResponseEntity save(@RequestBody UserEntity userEntity) { UserEntity result = userService.save(userEntity); return ResponseEntity.ok(result); }

public UserEntity save(UserEntity userEntity) { List orderEntityList = new ArrayList<>(); String userId = IdUtil.simpleUUID(); // 自定义Id覆盖 userEntity.setId(userId); // 创建嵌套对象 for (int i = 0; i < 4; i++) { OrderEntity orderEntity = new OrderEntity(); setProperties(orderEntity,i); orderEntity.setUserId(userId); orderEntityList.add(orderEntity); } userEntity.setOrderEntityList(orderEntityList); return userRepository.save(userEntity); }

最后直接调用UserRepositorysave方法即可完成保存,在上面的代码中我使用了自己的id规则来替代ES生成的 id。测试结果略。
2、查询 上面我们新增了一条数据,然后我们添加根据id查询结果,如下:
@PostMapping("/queryById/{id}") public ResponseEntity queryById(@PathVariable String id) { UserEntity result = userService.queryById(id); return ResponseEntity.ok(result); }

@Override public UserEntity queryById(String id) { Optional optional = userRepository.findById(id); return optional.isPresent() ? optional.get() : null; }

直接调用CrudRepository提供的findById即可。我们通过上面新增结果返回的id值进行查询,测试结果略。
3、删除 创建一个根据id删除的接口,如下:
@PostMapping("/deleteById/{id}") public ResponseEntity deleteById(@PathVariable String id) { userService.deleteById(id); return ResponseEntity.ok("success"); }

@Override public void deleteById(String id) { userRepository.deleteById(id); }

直接调用CrudRepository提供的deleteById即可。我们通过上面新增结果返回的id值进行删除,测试结果略。
4、修改接口 修改接口同新增,略。
5、分页查询 总的来看,如果是简单的增删改查操作CrudRepository都提供了相应的方法,直接使用就像而且使用起来都很简单。但是实际上我们的查询会有各种各样的条件,有模糊、精确、区间等等等,下面我们就来看一下条件查询。
其实分页查询在PagingAndSortingRepository接口中提供了一个方法,但是这个方法只能查询全部,这对我们来讲这个是不够的。接下来我们着重看一下条件查询,为了方便我就把条件查询和分页查询放到一起来演示。这种情况下可能就要通过使用ES的语法来完成了,不过我们可以选择使用Repositories或者ElasticsearchRestTemplate来完成了。下面我使用Repositories来完成,这种情况需要使用原生的ES语法。
我们先定一个分页条件查询的规则,比如:查询userAge在20到25之间,且userCode模糊匹配"2200",
创建一个接口分页条件查询的接口如下:
@PostMapping("/pageQuery") public ResponseEntity> pageQuery(@RequestBody QueryDTO queryDTO) { Page page = userService.pageQuery(queryDTO); return ResponseEntity.ok(page); }

@Override public Page pageQuery(QueryDTO queryDTO) { // 分页默认从0开始,按照userGrade逆向排序 PageRequest pageRequest = PageRequest.of(queryDTO.getPageNum() - 1,queryDTO.getPageSize(), Sort.by(Sort.Direction.DESC,"userAge")); Page page = null; // 条件查询 if (Boolean.TRUE.equals(queryDTO.getCondition())) { Integer min = queryDTO.getMinAge(); Integer max = queryDTO.getMaxAge(); String userCode = queryDTO.getUserCode(); page = userRepository.queryPage(userCode,min,max,pageRequest); } else { // 查询所有 page = userRepository.findAll(pageRequest); } return page; }

上面的代码中根据请求的分页参数创建了PageRequest对象,需要注意ES分页是从0开始的,所以我们用请求的页数减1。另外在Pageable接口中有一个默认的Sort对象用来排序,我们选择按照"userAge"逆向排序。排序和分页的参数全部封装在PageRequest中,查询时只需要传入即可。
我们在UserRepository定一个分页查询的接口,代码如下:
public interface UserRepository extends ElasticsearchRepository {@Query("{\"bool\": {\"must\": [{ \"query_string\": { \"default_field\": \"userCode\",\"query\": \"*?0*\"}},{ \"range\": {\"userAge\": {\"gte\": ?1,\"lte\": ?2}}}]}}") Page queryPage(String userCode,Integer min, Integer max, PageRequest pageRequest); }

这里使用了@Query注解,用来写原生的查询语句,参数传递上根据参数的顺序即可。提前向ES写入一些数据,接下来测试一下这个条件和分页查询。
先测试查询所有的结果
POST http://localhost:8080/user/pageQuery Accept: * Content-Type: application/json Cache-Control: no-cache{ "pageNum": 1,"pageSize": 20, "condition": false }

成功返回了结果,在测试下根据条件查询分页
POST http://localhost:8080/user/pageQuery Accept: * Content-Type: application/json Cache-Control: no-cache{ "pageNum": 1,"pageSize": 5, "condition": true,"userCode": "2200","minAge": 10,"maxAge": 30 }

查询结果也是成功的,结果这里就不再粘贴了。上面我们使用的是原生的ES语法,对于对ES语法不熟悉的小伙伴来说,可能有点麻烦,这时候可以考虑下使用elasticsearchRestTemplate来进行查询,感兴趣的不妨自己试一下。
三、总结
其实就使用Spring Data Elasticsearch来讲和Spring Data JPA有比较多的相似之处,个人感觉最主要的问题还是在ES本身。在学习的过程我觉得可以和传统的关系型数据库进行对比,找到二者之间相似点,这样更加方便理解。在本次学习中遇到了几个问题:
1、定义数据结构的时候,如果某个对象属性的@Fieldindex = false的话,这个属性是没办法作为一个查询的条件的,这里需要注意。
2、关于自动创建索引,即@DocumentcreateIndex除了自动创建索引也会进行映射,所以使用没必要手动创建,而且项目下次启动之后并不会影响原有的数据,我原来担心的是每次项目启动都会重新创建索引从而导致数据丢失,经过测试并不会。所以没有必要单独去创建索引。
3、自定义Repository中使用@Query注解时,直接从语法中query之后的内容开始写,我当时就是直接从kibana中复制的语句导致一直失败。拿分页查询举例:kibana中查询如下:
GETuser_entity_index/_search { "query": { "bool": { "must": [ { "range": {"userAge": {"gte": 20,"lte": 30} } }, { "query_string": { "default_field": "userCode", "query": "*2200*" } } ] } } }

大家可以对比上面UserRepository中的查询语句。
本次学习先到这里,最后我的代码会放在我的github。如果有什么问题也欢迎探讨,另外:我开了一个VX个人号:超超学堂,请大家多多关注,谢谢大家。

    推荐阅读