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如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ypc.spring.data</groupId>
    <artifactId>elastic</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>elastic</name>
    <description>ES project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.5.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

接着就是配置文件,主要是配置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<OrderEntity> 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<UserEntity,String> {

}

其继承了ElasticsearchRepository,当然网上追溯的话可以知道其实ElasticsearchRepository也是继承了CrudRepository接口的。

1、新增

我们创建一个新增的接口:

    @PostMapping("/save")
    public ResponseEntity<UserEntity> save(@RequestBody UserEntity userEntity) {
        UserEntity result = userService.save(userEntity);
        return ResponseEntity.ok(result);
    }
    public UserEntity save(UserEntity userEntity) {
        List<OrderEntity> 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<UserEntity> queryById(@PathVariable String id) {
        UserEntity result = userService.queryById(id);
        return ResponseEntity.ok(result);
    }
    @Override
    public UserEntity queryById(String id) {
        Optional<UserEntity> optional = userRepository.findById(id);
        return optional.isPresent() ? optional.get() : null;
    }

直接调用CrudRepository提供的findById即可。我们通过上面新增结果返回的id值进行查询,测试结果略。

3、删除

创建一个根据id删除的接口,如下:

    @PostMapping("/deleteById/{id}")
    public ResponseEntity<String> 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<Page<UserEntity>> pageQuery(@RequestBody QueryDTO queryDTO) {
        Page<UserEntity> page = userService.pageQuery(queryDTO);
        return ResponseEntity.ok(page);
    }
    @Override
    public Page<UserEntity> pageQuery(QueryDTO queryDTO) {
        // 分页默认从0开始,按照userGrade逆向排序
        PageRequest pageRequest = PageRequest.of(queryDTO.getPageNum() - 1,queryDTO.getPageSize(), Sort.by(Sort.Direction.DESC,"userAge"));
        Page<UserEntity> 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<UserEntity,String> {

    @Query("{\"bool\": {\"must\": [{ \"query_string\": { \"default_field\": \"userCode\",\"query\": \"*?0*\"}},{ \"range\": {\"userAge\": {\"gte\": ?1,\"lte\": ?2}}}]}}")
    Page<UserEntity> 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中查询如下:

GET  user_entity_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "range": {"userAge": {"gte": 20,"lte": 30} }
        },
        {
          "query_string": {
            "default_field": "userCode",
            "query": "*2200*"
          }
        }
      ]
    }
  }
}

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容