# Spring整合mongoDb

back:spring | back:mongoDB

修改pom文件

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-mongodb -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
<!--            <version>2.1.6.RELEASE</version>-->
        </dependency>
1
2
3
4
5
6

新增配置文件

#mongodb链接信息
spring.data.mongodb.uri=mongodb://htring:123456@localhost:27017/httest
#spring.data.mongodb.uri=mongodb://test:test@localhost:27017/httest
1
2
3

# SpringBoot中MongoDB注解概念及使用

# @Id

主键,不可重复,自带索引,可以在定义的列名上标注,需要自己生成并维护不重复的约束。如果自己不设置@Id主键,mongo会自动生成一个唯一主键,并且插入时效率远高于自己设置主键。

在实际业务中不建议自己设置主键,应交给mongo自己生成,自己可以设置一个业务id,如int型字段,用自己设置的业务id来维护相关联的表。

# @Document

标注在实体类上,类似于hibernate的entity注解,标明由mongo来维护该表。

# @Indexed

声明该字段需要加索引,加索引后以该字段为条件检索将大大提高速度。

  • 唯一索引的话是@Indexed(unique = true)
  • 也可以对数组进行索引,如果被索引的列是数组时,MongoDB会索引这个数组中的每一个元素。
  • 也可以对整个Document进行索引,排序是预定义的按插入BSON数据的先后升序排列。
  • 也可以对关联的对象的字段进行索引,譬如User对关联的address.city进行索引。

# @CompoundIndex

复合索引,加复合索引后通过复合索引字段查询将大大提高速度。

@Document
@CompoundIndexes({
    @CompoundIndex(name = "age_idx", def = "{'lastName': 1, 'age': -1}")
})
public class Person<T extends Address> {
}
1
2
3
4
5
6

# @Field

代表一个字段,可以不加,不加的话默认以参数名为列名。

# @Transient

被该注解标注的,将不会被录入到数据库中。只作为普通的javaBean属性。

# @DBRef

关联另一个document对象。类似于mysql的表关联,但并不一样,mongo不会做级联的操作。

  • 不会处理级联保存,你必须单独处理关联的对象。
  • 如果在Article里删除关联的list,set为null并保存,系统只会删掉Article里关联的list,而Picture对象本身的数据是不会被删除的
  • 譬如Article关联了两个空的Picture时在Article还能看到2个对象的引用,然后2个对象并不存在,是查询不出来的。

TIP

作用是在不同的表做划分吧,有点模拟mysql外键的意思。免得数据都落到一个大表的,不便于做关联的表的查询。

# 文档查询

back

@Getter
@Setter
@ToString
@Document
public class PersonModel implements Serializable {
    private static final long serialVersionUID = -3500393055197611669L;
    public static int varS = 0;
    @Id
    private String id;
    private String sex;
    private String firstName;
    private String lastName;
    private Integer age;
1
2
3
4
5
6
7
8
9
10
11
12
13

实体类中的注解解释如下:

  1. Document注解标识这是一个文档,等同mysql中的表,collection值表示mongodb中集合的名称,不写默认为实体类名article。
  2. Id注解为主键标识
  3. Field注解为字段标识,指定值为字段名称,这边有个小技巧,之所有spring-data.mongodb中有这样的注解,是为了能够让用户自定义字段名称,可以和实体类不一致,还有个好处就是可以用缩写,比如username我们可以配置成unane或者un,这样的好处是节省了存储空间,mongodb的存储方式是key value形式的,每个key就会重复存储,key其实就占了很大一份存储空间。

back

is正则ltgt排序

# 1、使用mongoTemplate对象常常会调用如下两种方法

findAll(Class<T> entityClass, String collectionName)
find(Query query, Class<T> entityClass, String collectionName)
1
2

# Query的使用

back

//封装查询条件
Criteria criteria = new Criteria();
//查询条件
Query query = new Query();

Query query = new Query(Criteria.where("alarm_name").is(name));
MongoAlarmBo mongoAlarmBo = this.mongoTemplate.findOne(query,MongoAlarmBo.class);
1
2
3
4
5
6
7
  • 注意
Criteria gte = Criteria.where("alarmTime").gte(mongoAlarmBoSearch.getBeginDate()).and("alarmTime").lte(mongoAlarmBoSearch.getEndDate());
1

上面这种写法是错误的,一个Criteria不可同时有两个同名的字段
必须写成下面这种形式:

Criteria gte = Criteria.where("alarmTime").gte(mongoAlarmBoSearch.getBeginDate());
Criteria lte = Criteria.where("alarmTime").lte(mongoAlarmBoSearch.getEndDate());
query.addCriteria(new Criteria().andOperator(gte,lte));
1
2
3

当然也可以写成以下形式,目的就是防止出现同一个字段:

Query query = new Query();
        Criteria gte = Criteria.where("alarmTime").gte(mongoAlarmBoSearch.getBeginDate()).lte(mongoAlarmBoSearch.getEndDate());
1
2

# LIKE查询

back

Criteria gte = Criteria.where("alarmTime").gte(mongoAlarmBoSearch.getBeginDate());
        if(!StringUtils.isEmptyOrWhitespace(mongoAlarmBoSearch.getSearchName())){
            //全匹配
//            Pattern pattern = Pattern.compile("^火$",Pattern.CASE_INSENSITIVE);
            //左匹配
//            Pattern pattern = Pattern.compile("^.*火$",Pattern.CASE_INSENSITIVE);
            //右匹配
//            Pattern pattern = Pattern.compile("^火.*$",Pattern.CASE_INSENSITIVE);
            //模糊匹配
            Pattern pattern = Pattern.compile("^.*火.*$",Pattern.CASE_INSENSITIVE);
            //等于
//            gte.and("alarmName").is(mongoAlarmBoSearch.getSearchName());
            gte.and("alarmName").regex(pattern);
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

简写:

gte.and("alarmName").regex("^.*"+mongoAlarmBoSearch.getSearchName());
1

# Example实现各种匹配查询

back

public List<MsgConfigBo> query(MsgConfigBo msgConfigBo) {
//        ExampleMatcher exampleMatcher = ExampleMatcher.matching().withMatcher("sendWay", GenericPropertyMatcher::contains);
        ExampleMatcher exampleMatcherWay = ExampleMatcher.matching()
                .withMatcher("sendWay", matcher -> matcher.contains());
//                .withIgnorePaths("sendKey");
        ExampleMatcher exampleMatcherKey = ExampleMatcher.matching()
                .withMatcher("sendKey",matcher -> matcher.contains())
                .withIgnorePaths("sendWay");
//        Criteria criteria = new Criteria();
//        criteria.and("sendWay").regex(".*?" + msgConfigBo.getSendWay() + ".*");
//        criteria.orOperator(Criteria.where("sendSecret").regex(".*?" + msgConfigBo.getSendSecret() + ".*"));
//        criteria.alike(Example.of(msgConfigBo));
//        List<MsgConfigBo> list= baseDao.queryByCriteria(criteria,MsgConfigBo.class);
        List<MsgConfigBo> list= baseDao
                .queryByCriteria(Criteria.byExample(Example.of(msgConfigBo,exampleMatcherWay))
                        .orOperator(Criteria.byExample(Example.of(msgConfigBo,exampleMatcherKey)))
                        ,MsgConfigBo.class);
        return list;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 排序

query.with(new Sort(Sort.Direction.ASC,"alarmTime"));
1

# 分页查询

back

实现类Pageable

package com.me.page;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import java.io.Serializable;

/**
 - @author huting
 */
public class PageableBoot implements Serializable, Pageable {


    private static final long serialVersionUID = -4084143006067667661L;

    private Integer pageNum = 1;
    private Integer page = 10;
    private Sort sort;

    public Integer getPageNum() {
        return pageNum;
    }

    public void setPageNum(Integer pageNum) {
        this.pageNum = pageNum;
    }

    public Integer getPage() {
        return page;
    }

    public void setPage(Integer page) {
        this.page = page;
    }

    public void setSort(Sort sort) {
        this.sort = sort;
    }

    /**
     - 当前页
     - @return
     */
    @Override
    public int getPageNumber() {
        return getPageNum();
    }

    /**
     - 每页记录数
     - @return
     */
    @Override
    public int getPageSize() {
        return getPage();
    }

    /**
     - 第二页所需增加偏移量
     - @return
     */
    @Override
    public long getOffset() {
        return (getPageNum()-1)*getPage();
    }

    @Override
    public Sort getSort() {
        return this.sort;
    }

    @Override
    public Pageable next() {
        return null;
    }

    @Override
    public Pageable previousOrFirst() {
        return null;
    }

    @Override
    public Pageable first() {
        return null;
    }

    @Override
    public boolean hasPrevious() {
        return false;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

查询

/**
     - 查询分页信息
     - @param pageNum
     - @return
     */
    public Page<MongoAlarmBo> queryPage(Integer pageNum){
        PageableBoot pageableBoot = new PageableBoot();
//        List<Sort.Order> orders = new ArrayList<>();
//        orders.add(new Sort.Order(Sort.Direction.ASC,"alarmName"));
        Query query = new Query();
        pageableBoot.setPageNum(pageNum);
        pageableBoot.setPage(3);
        pageableBoot.setSort(new Sort(Sort.Direction.ASC,"alarmName"));
        Long count = this.mongoTemplate.count(query,MongoAlarmBo.class);
//        query.with(new Sort(Sort.Direction.ASC,"alarmName"));
        List<MongoAlarmBo> lists = this.mongoTemplate.find(query.with(pageableBoot),
                MongoAlarmBo.class, AnnotationUtil.getCollectionName(MongoAlarmBo.class));
        Page<MongoAlarmBo> pageList = new PageImpl<>(lists,pageableBoot,count);
        return pageList;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 删除

back

//删除author为yinjihuan的数据

Query query = Query.query(Criteria.where("author").is("yinjihuan"));
mongoTemplate.remove(query, Article.class);
//如果实体类中没配集合名词,可在删除的时候单独指定article_info
query = Query.query(Criteria.where("author").is("yinjihuan"));
mongoTemplate.remove(query, "article_info");
//删除集合,可传实体类,也可以传名称
mongoTemplate.dropCollection(Article.class);
mongoTemplate.dropCollection("article_info");
//删除数据库
mongoTemplate.getDb().dropDatabase();

//查询出符合条件的第一个结果,并将符合条件的数据删除,只会删除第一条
query = Query.query(Criteria.where("author").is("yinjihuan"));
Article article = mongoTemplate.findAndRemove(query, Article.class);
//查询出符合条件的所有结果,并将符合条件的所有数据删除
query = Query.query(Criteria.where("author").is("yinjihuan"));
List<Article> articles = mongoTemplate.findAllAndRemove(query, Article.class);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# MongoRepository查询

back

写一个接口,继承MongoRepository,这个接口有了基本的CRUD的操作,当不能满足需求的时候,可以进行一些自定的方法,比如查询,findByName根据名字查询,findBySex,根据性别查询,在springboot中,我们只需要定义一个方法即可,springboot已经帮我们实现了。注意的是findByName需要严格的按照存入的MongoDB的字段对应。 继承MongoRepository

package com.me.dao;

import com.me.entity.MongoAlarmBo;
import org.springframework.data.mongodb.repository.MongoRepository;

import java.util.Date;
import java.util.List;

/**
 -  mongoDb告警信息相关操作接口
 -  @author huting
 */
public interface MongoAlarmBoDao extends MongoRepository<MongoAlarmBo,String> {
    /**
     - 通过告警名称查询告警信息
     - @param alarmName
     - @return
     */
    List<MongoAlarmBo> findByAlarmName(String alarmName);

    /**
     - 通过告警名称模糊查询告警信息
     - @param alarmName
     - @return
     */
    MongoAlarmBo queryFirstByAlarmNameLike(String alarmName);

    /**
     - 通过告警名称模糊查询所有相关告警信息
     - @param alarm
     - @return
     */
    List<MongoAlarmBo> findByAlarmNameContains(String alarm);

    /**
     - 根据告警时间查询数据
     - @param startDate
     - @param endDate
     - @return
     */
    List<MongoAlarmBo> findByAlarmTimeBetweenOrderByAlarmTimeDesc(Date startDate,Date endDate);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# NewSpring整合mongoDb

# 修改pom文件

back

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
<!--            <artifactId>spring-boot-starter-web</artifactId>-->
        </dependency>
1
2
3
4
5

# 实体类增加@Id注解

back

@Getter
@Setter
@ToString
@Document
public class PersonModel implements Serializable {
    private static final long serialVersionUID = -3500393055197611669L;
    public static int varS = 0;
    @Id
    private String id;
1
2
3
4
5
6
7
8
9

# 新建Dao

back

/**
 - @author huting
 */
@RepositoryRestResource(collectionResourceRel = "person",path = "person")
public interface PersonModelDao extends MongoRepository<PersonModel,String> {
    /**
     - 根据查询
     - @param firstName 名
     - @param lastName 姓
     - @return PersonModel
     */
    PersonModel findByFirstNameOrLastName(String firstName,String lastName);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

此时启动程序,访问http://localhost:8041/clm/person,可得结果:

{
  "_embedded" : {
    "person" : [ {
      "sex" : "男",
      "firstName" : "云",
      "lastName" : "赵",
      "age" : 19,
      "country" : "中国",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8041/clm/person/5d2be644de0bb421902aefe8"
        },
        "personModel" : {
          "href" : "http://localhost:8041/clm/person/5d2be644de0bb421902aefe8"
        }
      }
    }, {
      "sex" : "男",
      "firstName" : "云",
      "lastName" : "赵",
      "age" : 19,
      "country" : "中国",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8041/clm/person/5d2be645de0bb421902aefe9"
        },
        "personModel" : {
          "href" : "http://localhost:8041/clm/person/5d2be645de0bb421902aefe9"
        }
      }
    }, {
      "sex" : "男",
      "firstName" : "云w",
      "lastName" : "赵",
      "age" : 19,
      "country" : "中国",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8041/clm/person/5d2be667de0bb421902aefea"
        },
        "personModel" : {
          "href" : "http://localhost:8041/clm/person/5d2be667de0bb421902aefea"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8041/clm/person{?page,size,sort}",
      "templated" : true
    },
    "profile" : {
      "href" : "http://localhost:8041/clm/profile/person"
    },
    "search" : {
      "href" : "http://localhost:8041/clm/person/search"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 3,
    "totalPages" : 1,
    "number" : 0
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

我勒个去,不光是有数据集的返回结果 person: [ ] ,还附赠了一个links对象和page对象。如果你了解 Hypermedia 的概念,就会发现这是个符合 Hypermedia REST API返回的数据。
说两句关于 MongoRepository<Todo, String> 这个接口,前一个参数类型是领域对象类型,后一个指定该领域对象的Id类型

# Hypermedia REST

back

简单说两句Hypermedia是什么。简单来说它是可以让客户端清晰的知道自己可以做什么,而无需依赖服务器端指示你做什么。原理呢,也很简单,通过返回的结果中包括不仅是数据本身,也包括指向相关资源的链接。拿上面的例子来说(虽然这种默认状态生成的东西不是很有代表性):links中有一个profiles,我们看看这个profile的链接 http://localhost:8041/clm/profile/person 执行的结果是什么:

{ "alps" : { "version" : "1.0", "descriptor" : [ { "id" : "personModel-representation", "href" : "http://localhost:8041/clm/profile/person", "descriptor" : [ { "name" : "sex", "type" : "SEMANTIC" }, { "name" : "firstName", "type" : "SEMANTIC" }, { "name" : "lastName", "type" : "SEMANTIC" }, { "name" : "age", "type" : "SEMANTIC" }, { "name" : "country", "type" : "SEMANTIC" } ] }, { "id" : "get-person", "name" : "person", "type" : "SAFE", "descriptor" : [ { "name" : "page", "type" : "SEMANTIC", "doc" : { "format" : "TEXT", "value" : "The page to return." } }, { "name" : "size", "type" : "SEMANTIC", "doc" : { "format" : "TEXT", "value" : "The size of the page to return." } }, { "name" : "sort", "type" : "SEMANTIC", "doc" : { "format" : "TEXT", "value" : "The sorting criteria to use to calculate the content of the page." } } ], "rt" : "#personModel-representation" }, { "id" : "create-person", "name" : "person", "type" : "UNSAFE", "rt" : "#personModel-representation" }, { "id" : "update-personModel", "name" : "personModel", "type" : "IDEMPOTENT", "rt" : "#personModel-representation" }, { "id" : "delete-personModel", "name" : "personModel", "type" : "IDEMPOTENT", "rt" : "#personModel-representation" }, { "id" : "get-personModel", "name" : "personModel", "type" : "SAFE", "rt" : "#personModel-representation" }, { "id" : "patch-personModel", "name" : "personModel", "type" : "UNSAFE", "rt" : "#personModel-representation" }, { "name" : "findByFirstNameOrLastName", "type" : "SAFE", "descriptor" : [ { "name" : "firstName", "type" : "SEMANTIC" }, { "name" : "lastName", "type" : "SEMANTIC" } ] } ] } }
1

这个对象虽然我们暂时不是完全的理解,但大致可以猜出来,这个是person这个REST API的元数据描述,告诉我们这个API中定义了哪些操作和接受哪些参数等等。我们可以看到todo这个API有增删改查等对应功能。

其实呢,Spring是使用了一个叫 ALPS (alps.io/spec/index.… 的专门描述应用语义的数据格式。摘出下面这一小段来分析一下,这个描述了一个get方法,类型是 SAFE 表明这个操作不会对系统状态产生影响(因为只是查询),而且这个操作返回的结果格式定义在 #personModel-representation 中了。

#personModel-representation

{ "id" : "get-person", "name" : "person", "type" : "SAFE", "descriptor" : [ { "name" : "page", "type" : "SEMANTIC", "doc" : { "format" : "TEXT", "value" : "The page to return." } }, { "name" : "size", "type" : "SEMANTIC", "doc" : { "format" : "TEXT", "value" : "The size of the page to return." } }, { "name" : "sort", "type" : "SEMANTIC", "doc" : { "format" : "TEXT", "value" : "The sorting criteria to use to calculate the content of the page." } } ], "rt" : "#personModel-representation" }
1

执行后的结果如下,我们可以看到返回的links中包括了刚刚新增的personModel的link http://localhost:8041/clm/person/5d2be644de0bb421902aefe8 ( 5d2be644de0bb421902aefe8 就是数据库自动为这个personModel生成的Id),这样客户端可以方便的知道指向刚刚生成的personModel的API链接。 执行添加personModel后的返回Json数据
再举一个现实一些的例子,我们在开发一个“我的”页面时,一般情况下除了取得我的某些信息之外,因为在这个页面还会有一些可以链接到更具体信息的页面链接。如果客户端在取得比较概要信息的同时就得到这些详情的链接,那么客户端的开发就比较简单了,而且也更灵活了。

其实这个描述中还告诉我们一些分页的信息,比如每页20条记录(size: 20)总共几页(totalPages:1)总共多少个元素(totalElements: 1)当前第几页(number: 0)。当然你也可以在发送API请求时,指定page、size或sort参数。比如 http://localhost:8041/clm/person?size=2&page=0 就是指定每页2条,当前页是第一页(从0开始)。

# Hypermedia REST-魔法的背后

back

这么简单就生成一个有数据库支持的REST API,这件事看起来比较魔幻,但一般这么魔幻的事情总感觉不太托底,除非我们知道背后的原理是什么。首先再来回顾一下 PersonModelDao 的代码:

@RepositoryRestResource(collectionResourceRel = "person",path = "person")
public interface PersonModelDao extends MongoRepository<PersonModel,String> {
    /**
     - 根据查询
     - @param firstName 名
     - @param lastName 姓
     - @return PersonModel
     */
    PersonModel findByFirstNameOrLastName(String firstName,String lastName);
}
1
2
3
4
5
6
7
8
9
10

Spring是最早的几个IoC(控制反转或者叫DI)框架之一,所以最擅长的就是依赖的注入了。这里我们写了一个Interface,可以猜到Spring一定是有一个这个接口的实现在运行时注入了进去。如果我们去 spring-data-mongodb 的源码中看一下就知道是怎么回事了,这里只举一个小例子,大家可以去看一下 SimpleMongoRepository.java ( 源码链接 ),由于源码太长,我只截取一部分:

public class SimpleMongoRepository<T, ID extends Serializable> implements MongoRepository<T, ID> {

    private final MongoOperations mongoOperations;
    private final MongoEntityInformation<T, ID> entityInformation;

    /**
     - Creates a new {@link SimpleMongoRepository} for the given {@link MongoEntityInformation} and {@link MongoTemplate}.
     *
     - @param metadata must not be {@literal null}.
     - @param mongoOperations must not be {@literal null}.
     */
    public SimpleMongoRepository(MongoEntityInformation<T, ID> metadata, MongoOperations mongoOperations) {

        Assert.notNull(mongoOperations);
        Assert.notNull(metadata);

        this.entityInformation = metadata;
        this.mongoOperations = mongoOperations;
    }

    /*
     - (non-Javadoc)
     - @see org.springframework.data.repository.CrudRepository#save(java.lang.Object)
     */
    public <S extends T> S save(S entity) {

        Assert.notNull(entity, "Entity must not be null!");

        if (entityInformation.isNew(entity)) {
            mongoOperations.insert(entity, entityInformation.getCollectionName());
        } else {
            mongoOperations.save(entity, entityInformation.getCollectionName());
        }

        return entity;
    }
    ...
    public T findOne(ID id) {
        Assert.notNull(id, "The given id must not be null!");
        return mongoOperations.findById(id, entityInformation.getJavaType(), entityInformation.getCollectionName());
    }

    private Query getIdQuery(Object id) {
        return new Query(getIdCriteria(id));
    }

    private Criteria getIdCriteria(Object id) {
        return where(entityInformation.getIdAttribute()).is(id);
    }
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

也就是说其实在运行时Spring将这个类或者其他具体接口的实现类注入了应用。这个类中有支持各种数据库的操作。我了解到这步就觉得ok了,有兴趣的同学可以继续深入研究。

虽然不想在具体类上继续研究,但我们还是应该多了解一些关于 MongoRepository 的东西。这个接口继承了 PagingAndSortingRepository(定义了排序和分页)和QueryByExampleExecutor。而 PagingAndSortingRepository 又继承了 CrudRepository (定义了增删改查等)。

第二个魔法就是 @RepositoryRestResource(collectionResourceRel = "person", path = "person") 这个元数据的修饰了,它直接对MongoDB中的集合(本例中的person)映射到了一个REST URI(person)。因此我们连Controller都没写就把API搞出来了,而且还是个Hypermedia REST

其实呢,这个第二个魔法只在你需要变更映射路径时需要。本例中如果我们不加 @RepositoryRestResource 这个修饰符的话,同样也可以生成API,

@RepositoryRestResource
//(collectionResourceRel = "person",path = "person")
public interface PersonModelDao extends MongoRepository<PersonModel,String> {
    /**
     - 根据查询
     - @param firstName 名
     - @param lastName 姓
     - @return PersonModel
     */
    PersonModel findByFirstNameOrLastName(String firstName,String lastName);
}
1
2
3
4
5
6
7
8
9
10
11

只不过其路径按照默认的方式变成了 personModels ,大家可以试试把这个元数据修饰去掉,然后重启服务,访问 http://localhost:8041/clm/personModels 看看。

说到这里,顺便说一下REST的一些约定俗成的规矩。

一般来说如果我们定义了一个领域对象 (比如我们这里的PersonModel),那么这个对象的集合(比如PersonModel的列表)可以使用这个对象的命名的复数方式定义其资源URL,也就是刚刚我们访问的 http://localhost:8041/clm/personModels,对于新增一个对象的操作也是这个URL,但Request的方法是POST。
而这个某个指定的对象(比如指定了某个ID的PersonModel)可以使用 personModels/:id 来访问,比如本例中 http://localhost:8041/clm/personModels/5d2be645de0bb421902aefe9
对于这个对象的修改和删除使用的也是这个URL,只不过HTTP Request的方法变成了PUT(或者PATCH)和DELETE
这个里面默认采用的这个命名 personModels 是根据英语的语法来的,一般来说复数是加s即可,但比如这个box,是(辅音+o结尾、s、x结尾),所以采用的加es方式

# Hypermedia REST-无招胜有招

back

刚才我们提到的都是开箱即用的一些方法,你可能会想,这些东西看上去很炫,但没有毛用,实际开发过程中,我们要使用的肯定不是这么简单的增删改查啊。说的有道理,我们来试试看非默认方法。那么我们就来增加一个需求,我们可以通过查询PersonModel的描述中的关键字来搜索符合的项目。

显然这个查询不是默认的操作,那么这个需求在Spring Boot中怎么实现呢?非常简单,只需在 PersonModelDao 中添加一个方法:

...
List<PersonModel> findByFirstNameLike(@Param("firstName") String firstName);
}
1
2
3

太不可思议了,这样就行?不信可以启动服务后,在浏览器中输入 http://localhost:8041/clm/personModels/search/findByFirstNameLike?firstName=w 去看看结果。

{
  "_embedded" : {
    "personModels" : [ {
      "sex" : "男",
      "firstName" : "云w",
      "lastName" : "赵",
      "age" : 19,
      "country" : "中国",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8041/clm/personModels/5d2be667de0bb421902aefea"
        },
        "personModel" : {
          "href" : "http://localhost:8041/clm/personModels/5d2be667de0bb421902aefea"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8041/clm/personModels/search/findByFirstNameLike?firstName=w"
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

是的,我们甚至都没有写这个方法的实现就已经完成了该需求(题外话,其实 http://localhost:8041/clm/personModels?firstName=w 这个URL也起作用,可以试试)。

你说这里肯定有鬼,我同意。那么我们试试把这个方法改个名字 findDescLike ,果然不好用了。为什么呢?
这套神奇的疗法的背后还是那个我们在第一篇时提到的 Convention over configuration要神奇的疗效就得遵循Spring的配方
这个配方就是方法的命名是有讲究的:Spring提供了一套可以通过命名规则进行查询构建的机制
这套机制会把方法名首先过滤一些关键字,比如 find…By, read…By, query…By, count…By 和 get…By 。系统会根据关键字将命名解析成2个子语句,第一个 By 是区分这两个子语句的关键词。这个 By 之前的子语句是查询子语句(指明返回要查询的对象)后面的部分是条件子语句
如果直接就是 findBy… 返回的就是定义Respository时指定的领域对象集合(本例中的PersonModel组成的集合)。

一般到这里,有的同学可能会问 find…By, read…By, query…By, get…By 到底有什么区别啊?答案是。。。木有区别,就是别名,
从下面的定义可以看到这几个东东其实生成的查询是一样的,这种让你不用查文档都可以写对的方式也比较贴近目前流行的自然语言描述风格(类似各种DSL)。

private static final String QUERY_PATTERN = "find|read|get|query|stream";
1

刚刚我们实验了模糊查询,那如果要是精确查找怎么做呢,比如我们要筛选出已完成或未完成的PersonModel,也很简单:

  List<Todo> findByCompleted(@Param("completed") boolean completed);
1

# Hypermedia REST-嵌套对象的查询怎么搞

back

看到这里你会问,这都是简单类型,如果复杂类型怎么办?嗯,好的,我们还是增加一个需求看一下:现在需求是要这个API是多用户的,每个用户看到的PersonModel都是他们自己创建的项目。我们新建一个User领域对象:

@Getter
@Setter
@ToString
@Document
public class ExcelTestModel extends BaseRowModel implements Serializable {
    private static final long serialVersionUID = -3218181825840488143L;
    @Id
    private String id;
    @ExcelProperty(value = "姓名",index = 0)
    private String name;
    @ExcelProperty(value = "性别",index = 1)
    private String sex;
    @ExcelProperty(value = "年龄",index = 2)
    private Integer age;
    @ExcelProperty(value = "国家",index = 3)
    private String country;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

为了可以添加ExcelTestModel数据,我们需要一个ExcelTestModel的REST API,所以添加一个 ExcelTestModelDao

public interface ExcelTestModelDao extends MongoRepository<ExcelTestModel,String> {
}
1
2

然后给 PersonModel 领域对象添加一个ExcelTestModel属性。

public class PersonModel implements Serializable {
    private static final long serialVersionUID = -3500393055197611669L;
    public static int varS = 0;
    @Id
    private String id;
    private String sex;
    private String firstName;
    private String lastName;
    private Integer age;
    private String country;

    private ExcelTestModel excelTestModel;
1
2
3
4
5
6
7
8
9
10
11
12

接下来就可以来把 PersonModelDao 添加一个方法定义了,我们先实验一个简单点的,根据name来筛选出这个用户的PersonModel列表:

List<PersonModel> findByExcelName(@Param("name") String name);
1

我们在浏览器中输入 http://localhost:8041/clm/person/search/findByExcelName?name=云 ,我们会发现返回的结果中只有这个用户的PersonModel项目。

看到结果后我们来分析这个 findByExcelName 是如何解析的:

首先在 By 之后,解析器会按照 camel (每个单词首字母大写)的规则来分词。那么第一个词是 Excel,这个属性在 PersonModel 中有没有呢?有的,但是这个属性是另一个对象类型,所以紧跟着这个词的 Name 就要在 Excel 类中去查找是否有 Name 这个属性。

聪明如你,肯定会想到,那如果在 PersonModel 类中如果还有一个属性叫 excelName 怎么办?是的,这种情况下 excelName 会被优先匹配,此时请使用 _来显性分词处理这种混淆。

也就是说如果我们的 PersonModel 类中同时有 excel 和 excelName 两个属性的情况下,我们如果想要指定的是 excel 的 name ,那么需要写成 findByExcel_Name

还有一个问题,我估计很多同学现在已经在想了,那就是我们的这个例子中并没有使用 Excel 的 id,这不科学啊。是的,之所以没有在上面使用 findByExcelId 是因为要引出一个易错的地方,下面我们来试试看,将 PersonModelDao 的方法改成

public interface TodoRepository extends MongoRepository<Todo, String>{
    List<PersonModel> findByExcelId(@Param("excelId") String excelId);
}
1
2
3

你如果打开浏览器输入 http://localhost:8080/todos/search/findByUserId?userId=589089c3c5d0e2524e245458 (这里的Id请改成你自己mongodb中的user的id),你会发现返回的结果是个空数组。原因是虽然我们在类中标识 id 为 String 类型,但对于这种数据库自己生成维护的字段,它在MongoDB中的类型是ObjectId,所以在我们的接口定义的查询函数中应该标识这个参数是 ObjectId。那么我们只需要改动 excelId 的类型为 org.bson.types.ObjectId 即可。

    List<PersonModel> findByexcelId(@Param("userId") ObjectId excelId);
1

# Hypermedia REST-再复杂一些行不行

back

好吧,到现在我估计还有一大波攻城狮表示不服,实际开发中需要的查询比上面的要复杂的多,再复杂一些怎么办?还是用例子来说话吧,那么现在我们想要模糊搜索指定用户的PersonModel中描述的关键字,返回匹配的集合。

这个需求我们只需改动一行,这个以命名规则为基础的查询条件是可以加 And、Or 这种关联多个条件的关键字的。

List<Todo> findByUserIdAndDescLike(@Param("userId") ObjectId userId, @Param("desc") String desc);
1

当然,还有其他操作符:Between (值在两者之间), LessThan (小于), GreaterThan (大于), Like (包含), IgnoreCase (b忽略大小写), AllIgnoreCase (对于多个参数全部忽略大小写), OrderBy (引导排序子语句), Asc (升序,仅在 OrderBy 后有效) 和 Desc (降序,仅在 OrderBy 后有效)
刚刚我们谈到的都是对于查询条件子语句的构建,其实在 By 之前,对于要查询的对象也可以有限定的修饰词 Distinct (去重,如有重复取一个值)。比如有可能返回的结果有重复的记录,可以使用 findDistinctTodoByUserIdAndDescLike
我可以直接写查询语句吗?几乎所有码农都会问的问题。当然可以咯,也是同样简单,
就是给方法加上一个元数据修饰符 @Query

public interface TodoRepository extends MongoRepository<Todo, String>{
    @Query("{ 'user._id': ?0, 'desc': { '$regex': ?1} }")
    List<Todo> searchTodos(@Param("userId") ObjectId userId, @Param("desc") String desc);
}
1
2
3
4

采用这种方式我们就不用按照命名规则起方法名了,可以直接使用MongoDB的查询进行。上面的例子中有几个地方需要说明一下

?0?1 是参数的占位符,?0 表示第一个参数,也就是 userId;?1 表示第二个参数也就是 desc。
使用user._id 而不是 user.id 是因为所有被 @Id 修饰的属性在Spring Data中都会被转换成 _id
MongoDB中没有关系型数据库的Like关键字,需要以正则表达式的方式达成类似的功能

其实,这种支持的力度已经可以让我们写出相对较复杂的查询了。但肯定还是不够的,对于开发人员来讲,如果不给可以自定义的方式基本没人会用的,因为总有这样那样的原因会导致我们希望能完全掌控我们的查询或存储过程。

  • 方法名根据字段名即可

# 分页查询2

back

任何查询方法里都可添加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
2
3
4

对于分页查询,返回的对象也可用Slice<User>来代替,这个会轻量很多。也可以直接返回一个list,这样就不会触发分页信息的查询和统计

# 限制结果集数量

back

查询方法的结果可以通过使用firsttop关键字来限制,可以互换使用。可以附加一个可选的数值,top或者first指定要返回的最大结果大小。

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);
1
2
3
4
5
6

对于将结果集限制为一个实例的查询,支持将结果包装到Optional关键字中。

# 流式查询结果

back

可以使用Java 8 Stream<T>作为返回类型以递增方式处理查询方法的结果。而不是将查询结果包装在Stream数据存储中的特定方法用于执行流式传输,

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

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);
1
2
3
4
5
6
7

Stream潜在包装底层数据存储专用资源,因此必须在使用之后被关闭。您可以Stream使用该close()方法或使用Java 7 try-with-resources块手动关闭,如以下示例所示

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

# 异步查询

back

@Async
Future<User> findByFirstname(String firstname);
@Async
CompletableFuture<User> findOneByFirstname(String firstname);
@Async
ListenableFuture<User> findOneByLastname(String lastname);
1
2
3
4
5
6

# 多数据源

back

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

# 多表关联查询

back

# 一对一:两表关联查询