最近在项目开发过程中发现项目接口调用日志表存在一定的问题,为了记录项目中所有的接口调用数据专门用了一个表来存储请求接口的报文信息,一直以来也没出现什么问题,上次我在和外部系统对接时发现,该接口返回的数据比较大,少的时候也有几百Kb,这就导致了日志存储这一点存在问题,这么大的数据使用mysql
感觉已经不能满足开发的需要了,所以我就想能不能换一种方式来存储,比如ES
或者MongoDB
。最终我还是选择了ES
,一是项目中已经在使用ES
;二是单独搭建一个MongoDB
就存储一个表的数据感觉有点浪费。今天就来学习一下使用ES
来存储数据,并实现增删改查的功能。
之前自己也使用过使用ES
来代替传统的关系型数据库,可以看文章:ES使用遇到的问题。但是因为版本升级,之前的一些API已经是过时了,所以我决定在新版本的基础上重新来学习一下。
一、项目准备
首先说一下本次使用的ES
是7.6.0,Spring Boot
则是2.4.0,因为不同的版本在使用的过程中还是会有一些差别,这点大家注意一下。文档地址:https://docs.spring.io/spring-data/elasticsearch/docs/3.2.12.RELEASE/reference/html。
按照惯例还是创建一个简单的Spring Boot
项目,并引入必要的依赖,比如ES
,这里说一下我建议直接使用Spring Data Elasticsearch
,项目pom.xml
如下:
1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4 <modelVersion>4.0.0</modelVersion>
5 <parent>
6 <groupId>org.springframework.boot</groupId>
7 <artifactId>spring-boot-starter-parent</artifactId>
8 <version>2.4.0</version>
9 <relativePath/> <!-- lookup parent from repository -->
10 </parent>
11 <groupId>com.ypc.spring.data</groupId>
12 <artifactId>elastic</artifactId>
13 <version>1.0-SNAPSHOT</version>
14 <name>elastic</name>
15 <description>ES project for Spring Boot</description>
16
17 <properties>
18 <java.version>1.8</java.version>
19 </properties>
20
21 <dependencies>
22 <dependency>
23 <groupId>org.springframework.boot</groupId>
24 <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
25 </dependency>
26 <dependency>
27 <groupId>org.springframework.boot</groupId>
28 <artifactId>spring-boot-starter-web</artifactId>
29 </dependency>
30 <dependency>
31 <groupId>cn.hutool</groupId>
32 <artifactId>hutool-all</artifactId>
33 <version>5.5.2</version>
34 </dependency>
35 <dependency>
36 <groupId>org.projectlombok</groupId>
37 <artifactId>lombok</artifactId>
38 <optional>true</optional>
39 </dependency>
40 <dependency>
41 <groupId>org.springframework.boot</groupId>
42 <artifactId>spring-boot-starter-test</artifactId>
43 <scope>test</scope>
44 </dependency>
45 </dependencies>
46
47 <build>
48 <plugins>
49 <plugin>
50 <groupId>org.springframework.boot</groupId>
51 <artifactId>spring-boot-maven-plugin</artifactId>
52 </plugin>
53 </plugins>
54 </build>
55
56</project>
接着就是配置文件,主要是配置ES
地址、用户名、密码等,这个和之前配置是不一样的,如下:
1spring.elasticsearch.rest.uris=localhost:9200
2spring.elasticsearch.rest.connection-timeout=6s
3spring.elasticsearch.rest.read-timeout=10s
4# spring.elasticsearch.rest.password=
5# spring.elasticsearch.rest.username=
因为我本地ES
没有设置用户名和密码,所以就略去了。
接下我们需要创建我们的数据结构,这个和原来基本是一样的。比如我创建一个UserEntity
,如下:
1@Data
2@Document(indexName = "user_entity_index",shards = 1,replicas = 1,createIndex = false)
3public class UserEntity {
4
5 @Id
6 private String id;
7
8 @Field(type = FieldType.Keyword, store = true)
9 private String userName;
10
11 @Field(type = FieldType.Keyword, store = true)
12 private String userCode;
13
14 @Field(type = FieldType.Text, store = true,index = false)
15 private String userAddress;
16
17 @Field(type = FieldType.Keyword, store = true)
18 private String userMobile;
19
20 @Field(type = FieldType.Integer, store = true)
21 private Integer userGrade;
22
23 @Field(type = FieldType.Nested, store = true)
24 private List<OrderEntity> orderEntityList;
25
26 @Field(type = FieldType.Keyword, store = true)
27 private String status;
28
29 @Field(type = FieldType.Integer, store = true,index = false)
30 private Integer userAge;
31}
1@Data
2public class OrderEntity {
3 @Field(type = FieldType.Keyword, store = true)
4 private String id;
5 @Field(type = FieldType.Keyword, store = true,index = false)
6 private String orderNum;
7 @Field(type = FieldType.Date,format = DateFormat.custom,pattern = "yyyy-MM-dd HH:mm:ss",store = true)
8 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
9 private Date createTime;
10 @Field(type = FieldType.Date,format = DateFormat.custom,pattern = "yyyy-MM-dd HH:mm:ss",store = true)
11 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
12 private Date updateTime;
13 @Field(type = FieldType.Keyword, store = true)
14 private String amount;
15 @Field(type = FieldType.Keyword, store = true)
16 private String userId;
17 @Field(type = FieldType.Keyword, store = true)
18 private String mobile;
19 @Field(type = FieldType.Keyword, store = true)
20 private String status;
21}
@Document
注解和使用Spring Data JPA
中的@Entity
是比较相似的,这个注解定义了索引的名称,分片和备份的数量,还有是否创建索引,我这里选择否,即不自动创建索引,这个下面再说。@Field
则可以对比@Column
,这里定义了这个属性的数据类型,是否存储和是否索引。ES
支持数据类型还是很多的,对于正常的使用足够了。另外我这里还定义了一个Nested
,即一个对象列表,后面我们在看这块内容。对于日期类型,如果是自定义的,必须指定pattern
。
创建好数据模型之后我们还要做一件事情,就是索引还有就是映射关系,单独只创建索引是不行的,就好比mysql
你创建了数据库,你还需要创建表。当然索引和映射关系手动创建也可以,我通过实现ApplicationRunner
接口来创建,代码如下:
1@Slf4j
2@Component
3public class UserRunner implements ApplicationRunner {
4
5 @Autowired
6 private ElasticsearchOperations elasticsearchOperations;
7
8 @Override
9 public void run(ApplicationArguments args) throws Exception {
10 IndexCoordinates indexCoordinates = IndexCoordinates.of("user_entity_index");
11 IndexOperations indexOperations = elasticsearchOperations.indexOps(indexCoordinates);
12 if (!indexOperations.exists()) {
13 // 创建索引
14 indexOperations.create();
15 indexOperations.refresh();
16 // 将映射关系写入到索引,即将数据结构和类型写入到索引
17 indexOperations.putMapping(UserEntity.class);
18 indexOperations.refresh();
19 log.info(">>>> 创建索引和映射关系成功 <<<<");
20 }
21 }
22}
之前会使用ElasticsearchRestTemplate
来创建索引和映射,但是新的版本已经过时了,官方推荐使用ElasticsearchOperations
。首先这里创建的索引一定要和@Document
注解上的索引保持一致。ApplicationRunner
会在项目启动成功之后运行,所以第一次启动项目之后会自动创建索引并进行映射,然后查看下我们创建的索引以及映射,这里就略过了。接下来我们进行简单的CRUD
。
二、增删改查
在使用ES
进行操作的时候,我们其实可以使用Elasticsearch Repositories
也可以使用ElasticsearchOperations
接口,当然对于ES
的语法不太熟悉且操作比较简单的我建议使用Repositories
,因为它在使用上比较简单,如果你又使用过Spring Data JPA
的话上手非常的容易。
这里说一下数据结构上的@Id
注解,其和传统的数据库主键的作用是一样的,默认的话ES
会在后端自动生产一个UUID
,当然也可以自己赋值去覆盖。
我们创建一个Repositories
接口,如下:
1public interface UserRepository extends ElasticsearchRepository<UserEntity,String> {
2
3}
其继承了ElasticsearchRepository
,当然网上追溯的话可以知道其实ElasticsearchRepository
也是继承了CrudRepository
接口的。
1、新增
我们创建一个新增的接口:
1 @PostMapping("/save")
2 public ResponseEntity<UserEntity> save(@RequestBody UserEntity userEntity) {
3 UserEntity result = userService.save(userEntity);
4 return ResponseEntity.ok(result);
5 }
1 public UserEntity save(UserEntity userEntity) {
2 List<OrderEntity> orderEntityList = new ArrayList<>();
3 String userId = IdUtil.simpleUUID();
4 // 自定义Id覆盖
5 userEntity.setId(userId);
6 // 创建嵌套对象
7 for (int i = 0; i < 4; i++) {
8 OrderEntity orderEntity = new OrderEntity();
9 setProperties(orderEntity,i);
10 orderEntity.setUserId(userId);
11 orderEntityList.add(orderEntity);
12 }
13 userEntity.setOrderEntityList(orderEntityList);
14 return userRepository.save(userEntity);
15 }
最后直接调用UserRepository
的save
方法即可完成保存,在上面的代码中我使用了自己的id
规则来替代ES
生成的 id
。测试结果略。
2、查询
上面我们新增了一条数据,然后我们添加根据id
查询结果,如下:
1 @PostMapping("/queryById/{id}")
2 public ResponseEntity<UserEntity> queryById(@PathVariable String id) {
3 UserEntity result = userService.queryById(id);
4 return ResponseEntity.ok(result);
5 }
1 @Override
2 public UserEntity queryById(String id) {
3 Optional<UserEntity> optional = userRepository.findById(id);
4 return optional.isPresent() ? optional.get() : null;
5 }
直接调用CrudRepository
提供的findById
即可。我们通过上面新增结果返回的id
值进行查询,测试结果略。
3、删除
创建一个根据id
删除的接口,如下:
1 @PostMapping("/deleteById/{id}")
2 public ResponseEntity<String> deleteById(@PathVariable String id) {
3 userService.deleteById(id);
4 return ResponseEntity.ok("success");
5 }
1 @Override
2 public void deleteById(String id) {
3 userRepository.deleteById(id);
4 }
直接调用CrudRepository
提供的deleteById
即可。我们通过上面新增结果返回的id
值进行删除,测试结果略。
4、修改接口
修改接口同新增,略。
5、分页查询
总的来看,如果是简单的增删改查操作CrudRepository
都提供了相应的方法,直接使用就像而且使用起来都很简单。但是实际上我们的查询会有各种各样的条件,有模糊、精确、区间等等等,下面我们就来看一下条件查询。
其实分页查询在PagingAndSortingRepository
接口中提供了一个方法,但是这个方法只能查询全部,这对我们来讲这个是不够的。接下来我们着重看一下条件查询,为了方便我就把条件查询和分页查询放到一起来演示。这种情况下可能就要通过使用ES
的语法来完成了,不过我们可以选择使用Repositories
或者ElasticsearchRestTemplate
来完成了。下面我使用Repositories
来完成,这种情况需要使用原生的ES
语法。
我们先定一个分页条件查询的规则,比如:查询userAge
在20到25之间,且userCode
模糊匹配"2200",
创建一个接口分页条件查询的接口如下:
1 @PostMapping("/pageQuery")
2 public ResponseEntity<Page<UserEntity>> pageQuery(@RequestBody QueryDTO queryDTO) {
3 Page<UserEntity> page = userService.pageQuery(queryDTO);
4 return ResponseEntity.ok(page);
5 }
1 @Override
2 public Page<UserEntity> pageQuery(QueryDTO queryDTO) {
3 // 分页默认从0开始,按照userGrade逆向排序
4 PageRequest pageRequest = PageRequest.of(queryDTO.getPageNum() - 1,queryDTO.getPageSize(), Sort.by(Sort.Direction.DESC,"userAge"));
5 Page<UserEntity> page = null;
6 // 条件查询
7 if (Boolean.TRUE.equals(queryDTO.getCondition())) {
8 Integer min = queryDTO.getMinAge();
9 Integer max = queryDTO.getMaxAge();
10 String userCode = queryDTO.getUserCode();
11 page = userRepository.queryPage(userCode,min,max,pageRequest);
12 } else {
13 // 查询所有
14 page = userRepository.findAll(pageRequest);
15 }
16 return page;
17 }
上面的代码中根据请求的分页参数创建了PageRequest
对象,需要注意ES
分页是从0开始的,所以我们用请求的页数减1。另外在Pageable
接口中有一个默认的Sort
对象用来排序,我们选择按照"userAge"逆向排序。排序和分页的参数全部封装在PageRequest
中,查询时只需要传入即可。
我们在UserRepository
定一个分页查询的接口,代码如下:
1public interface UserRepository extends ElasticsearchRepository<UserEntity,String> {
2
3 @Query("{\"bool\": {\"must\": [{ \"query_string\": { \"default_field\": \"userCode\",\"query\": \"*?0*\"}},{ \"range\": {\"userAge\": {\"gte\": ?1,\"lte\": ?2}}}]}}")
4 Page<UserEntity> queryPage(String userCode,Integer min, Integer max, PageRequest pageRequest);
5}
这里使用了@Query
注解,用来写原生的查询语句,参数传递上根据参数的顺序即可。提前向ES
写入一些数据,接下来测试一下这个条件和分页查询。
先测试查询所有的结果
1POST http://localhost:8080/user/pageQuery
2Accept: *
3Content-Type: application/json
4Cache-Control: no-cache
5
6{
7 "pageNum": 1,"pageSize": 20, "condition": false
8}
成功返回了结果,再测试下根据条件查询分页
1POST http://localhost:8080/user/pageQuery
2Accept: *
3Content-Type: application/json
4Cache-Control: no-cache
5
6{
7 "pageNum": 1,"pageSize": 5, "condition": true,"userCode": "2200","minAge": 10,"maxAge": 30
8}
查询结果也是成功的,结果这里就不再粘贴了。上面我们使用的是原生的ES
语法,对于对ES
语法不熟悉的小伙伴来说,可能有点麻烦,这时候可以考虑下使用elasticsearchRestTemplate
来进行查询,感兴趣的不妨自己试一下。
三、总结
其实就使用Spring Data Elasticsearch
来讲和Spring Data JPA
有比较多的相似之处,个人感觉最主要的问题还是在ES
本身。在学习的过程我觉得可以和传统的关系型数据库进行对比,找到二者之间相似点,这样更加方便理解。在本次学习中遇到了几个问题:
1、定义数据结构的时候,如果某个对象属性的@Field
中index = false
的话,这个属性是没办法作为一个查询的条件的,这里需要注意。
2、关于自动创建索引,即@Document
中createIndex
除了自动创建索引也会进行映射,所以使用没必要手动创建,而且项目下次启动之后并不会影响原有的数据,我原来担心的是每次项目启动都会重新创建索引从而导致数据丢失,经过测试并不会。所以没有必要单独去创建索引。
3、自定义Repository
中使用@Query
注解时,直接从语法中query
之后的内容开始写,我当时就是直接从kibana
中复制的语句导致一直失败。拿分页查询举例:kibana
中查询如下:
1GET user_entity_index/_search
2{
3 "query": {
4 "bool": {
5 "must": [
6 {
7 "range": {"userAge": {"gte": 20,"lte": 30} }
8 },
9 {
10 "query_string": {
11 "default_field": "userCode",
12 "query": "*2200*"
13 }
14 }
15 ]
16 }
17 }
18}
大家可以对比上面UserRepository
中的查询语句。
本次学习先到这里,最后我的代码会放在我的github。如果有什么问题也欢迎探讨,最后请大家多多点赞、分享和转发,谢谢大家。




