引言
前面的两篇文章已经将MongoDB
大多数知识进行了阐述,不过其中的所有内容,都基于原生的MongoDB
语法在操作。可是,在实际的日常开发过程中,我们并不会直接去接触MongoDB
,毕竟MongoDB
只能算作是系统内的一个组件,无法仅依靠它来搭建出一整套系统。
我们都是高级语言的开发者,就如同MySQL
一样,在不同语言中,都会有各自的驱动、ORM
框架,MongoDB
亦是如此,而在Java
中如何使用MongoDB
呢?总共有两种方案:
- ①
MongoDB-Driver
:官方提供的数据库驱动,可以基于此执行各种MongoDB
支持的操作; - ②
Spring-Data-MongoDB
:Spring
对原生驱动的封装,提供了更简易的API
。
通常来说,我们一般不会使用第一种方式操作MongoDB
,类比MySQL
,第一种方案就相当于原生的JDBC
,而第二种方案就相当于MyBatis/Plus、JPA、Hibernate
这种持久层框架。两者相比较,显然后者对开发者更加友好,也能在极大程度上提升开发效率,从而满足Boss
的"快速开发"理念。
好了,本文会以Spring-Data-MongoDB
作为客户端框架,一点点阐述在Java
中如何使用MongoDB
提供的众多命令,那么,下面开始吧~
PS:个人编写的《技术人求职指南》小册已完结,其中从技术总结开始,到制定期望、技术突击、简历优化、面试准备、面试技巧、谈薪技巧、面试复盘、选
Offer
方法、新人入职、进阶提升、职业规划、技术管理、涨薪跳槽、仲裁赔偿、副业兼职......,为大家打造了一套"从求职到跳槽"的一条龙服务,同时也为诸位准备了七折优惠码:3DoleNaE
,感兴趣的小伙伴可以点击:s.juejin.cn/ds/USoa2R3/了解详情!
其实当你掌握了Spring-Data-MongoDB
的用法,也就掌握了SpringData
系列的框架用法,比如Spring-Data-JPA
,用起来和本文讲的Spring-Data-MongoDB
没有太大区别,因为SpringData
体系下的大多数框架,核心设计是共通的,如果你之前对SpringData
不熟悉,本文也能助你掌握这方面的技术栈~
一、SpringData-MongoDB入门
如果使用过SpringData-JPA
的小伙伴,在使用SpringData-MongoDB
会有种异常的熟悉感,因为它们都出自于Spring
组织,所以秉持着相同的框架风格,为什么这么说呢?一点点往下看就明白了。
这里先基于SpringBoot
快速搭建出一个Java
工程,接着在pom.xml
中引入下述依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
这里使用的是spring-boot-starter
,所以无需指定版本号,默认会跟SpirngBoot
版本保持一致,我这里SpringBoot
版本为2.7.15
,接着可以配置一下application.yml
文件,如下:
yml
spring:
data:
mongodb:
uri: mongodb://zhuzi:123456@192.168.229.136:27017/zhuzi
# 也可以分开写
# host: 192.168.229.136
# port: 27017
# database: zhuzi
# username: zhuzi
# password: 123456
这段配置特别容易看懂,所以不做过多解释,配置完成后,为了方便后续操作,这里再引入一下Lombok
的依赖:
xml
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.27</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
好了,依赖、配置都弄好后,接着来编写与MongoDB
集合相映射的实体类,如下:
java
@Data
@Document(collection = "xiong_mao")
public class Panda implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Integer id;
private String name;
private Integer age;
private String color;
private Food food;
}
@Data
public class Food implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String grade;
}
这里没啥太值得注意的,和MyBatisPlus
也很相似,由于目前实体类名和集合名不一样,因此使用@Document
显示指定一下集合名词,其次用@Id
注解显示指定出主键。接着来看food
属性,因为这是一个嵌入文档,所以咱们又定义了另外一个类Food
,并将其作为了Panda
类的一个属性,SpringData
在映射时,会拿着food
属性名+Food
类里的属性名拼接,形成food.name
这样的格式,正好和MongoDB
嵌入文档的使用方式相同。
实体类搞好后,接着来写经典的MVC
三层,目前仅是为了测通,为此,只编写持久层和业务层,这里先写前者:
java
@Repository
public interface PandaRepository extends MongoRepository<Panda, Integer> {}
如你所见,持久层仅仅定义了一个接口,而后继承了SpringData
提供的MongoRepository
接口,并传递了两个泛型,前者代表与当前Repository
绑定的实体类(集合),后者表示集合内的主键类型。除此之外,咱们什么都不用写,因为SpringData
会通过动态代理的方式,帮我们生成基础的CRUD
方法。
接着来编写Service
层,遵循传统的项目编码风格,先定义接口,再撰写实现类,接口定义如下:
java
public interface PandaService {
void save(Panda panda);
void deleteById(Integer id);
void update(Panda panda);
Panda findById(Integer id);
List<Panda> findAll();
}
其中定义了五个基本的增删改查方法,注释也没写,毕竟一眼就能看懂的代码,接着看看实现类:
java
@Service
public class PandaServiceImpl implements PandaService {
@Autowired
private PandaRepository pandaRepository;
@Override
public void save(Panda panda) {
pandaRepository.save(panda);
}
@Override
public void deleteById(Integer id) {
pandaRepository.deleteById(id);
}
@Override
public void update(Panda panda) {
pandaRepository.save(panda);
}
@Override
public Panda findById(Integer id) {
return pandaRepository.findById(id).get();
}
@Override
public List<Panda> findAll() {
return pandaRepository.findAll();
}
}
又是一眼能看懂的代码,这里将前面定义的PandaRepository
注入了进来,而后实现了PandaService
接口的每个方法,每个方法中没有包含任何业务逻辑,只是单纯调了pandaRepository
的默认方法,不过这里注意:update
修改方法,调用的也是pandaRepository.save()
方法实现,这是为什么呢?大家可以点进save()
方法看看:
此时不难发现,这些方法最终会调用到CrudRepository
接口提供的基本方法,而这个CRUD
接口中,并没有提供update()
系列的方法,而save()
插入相同_id
的数据时,会直接覆盖上一次的数据,为此,我们可以通过该方法间接实现修改功能,只不过每次修改之前,需要先find
一次将原数据查询出来。
好了,剩下的就是编写测试用例了,这一步更简单,如下:
java
@SpringBootTest
public class PandaServiceTest {
@Autowired
private PandaService pandaService;
@Test
void testSave() {
Panda panda = new Panda();
panda.setId(111);
panda.setName("黑黑");
panda.setColor("黑色");
panda.setAge(8);
Food food = new Food();
food.setName("黑晶竹");
food.setGrade("A");
panda.setFood(food);
pandaService.save(panda);
}
@Test
void testDelete() {
pandaService.deleteById(111);
}
@Test
void testFindById() {
Panda panda = pandaService.findById(111);
System.out.println(panda);
}
@Test
void TestUpdate() {
Panda panda = pandaService.findById(111);
panda.setAge(9);
pandaService.update(panda);
}
@Test
void testFindAll(){
List<Panda> pandas = pandaService.findAll();
pandas.forEach(System.out::println);
}
}
该测试类中,将咱们定义的PandaService
注入了进来,而后为每一个service
方法写了用例,具体的执行结果就不贴了,诸位可以自己执行一下看看结果,其实和操作MySQL
没有太大差异。
二、SpringData-MongoDB进阶
OK,在前面简单过了一下SpringData-MongoDB
基础的增删改查操作,但实际开发过程中,只掌握这些远远不够,面对复杂多变的业务场景,咱们需要灵活使用不同API
来满足需求,为此,接下来会一点点继续拆解SpringData-MongoDB
的其他API
。
2.1、实体类注解
前面在定义实体类时,简单介绍了@Id、@Document
两个注解,这里先全面介绍一些常用注解。
@Document
:作用于类上面,被该注解修饰的类,会和MongoDB
中的集合相映射,如果类名和集合名不一致,可以通过collection
参数来指定。@Id
:标识一个字段为主键,可以加在任意字段上,但如果该字段不为_id
,每次插入需要自己生成全局唯一的主键;如果不设置@Id
主键,MongoDB
会默认插入一个_id
值来作为主键。@Transient
:被该注解修饰的属性,在CRUD
操作发生时,SpringData
会自动将其忽略,不会被传递给MongoDB
。@Field
:作用于普通属性上,如果Java
属性名和MongoDB
字段名不一致,可以通过该注解来做别名映射。@DBRef
:一般用来修饰"嵌套文档"字段,主要用于关联另一个文档。@Indexed
:可作用于任意属性上,被该注解修饰的属性,如果MongoDB
中还未创建索引,在第一次插入时,SpringData
会默认为其创建一个普通索引。@CompoundIndex
:作用于类上,表示创建复合索引,可以通过name
参数指定索引名,def
参数指定组成索引的字段及排序方式。@GeoSpatialIndexed、@TextIndexed
:和上面的@Indexed
注解作用相同,前者代表空间索引,后者代表全文索引。
好了,上面这些便是SpringData-MongoDB
提供用于修饰实体类的注解,其实除此之外,还有些由SpingData
提供的通用注解,但这里不再例举,毕竟上述这些已经够用了。
2.2、自定义方法
经过前面的案例,会发现SpringData-MongoDB
提供了默认的CRUD
方法,那如果现在有个需求,没有对应的默认方法怎么办?这时可以自定义方法,但SpringData
是以方法命名来决定方法怎么执行的,规范如下:
findBy<fieldName>
:根据指定的单个条件进行等值查询;findBy<fieldName>And<fieldName>And<...>
:根据指定的多条件进行and
查询;findBy<fieldName>Or<fieldName>Or<...>
:根据指定的多条件进行or
查询;findBy<fieldName>Equals
:根据指定的单个条件进行等值查询;findBy<fieldName>In
:对指定的单个字段进行in
查询,入参为一个列表;findBy<fieldName>Like
:对指定的单个字段进行like
模糊查询;findBy<fieldName>NotNull
:查询指定字段不为空的数据;findBy<fieldName>GreaterThan
:对指定的单个字段进行>
范围查询;findBy<fieldName>GreaterThanEqual
:对指定的单个字段进行>=
范围查询;findBy<fieldName>LessThan
:对指定的单个字段进行<
范围查询;findBy<fieldName>LessThanEqual
:对指定的单个字段进行<=
范围查询;Page<...> findBy<...>
:根据指定的条件进行分页查询;countBy<fieldName>
:根据指定的条件字段进行计数统计;findTop<n>By<fieldName>
:根据指定字段做等值查询,并返回前n
条数据;findBy<fieldName>Between
:根据指定字段进行between
范围查询;findDistinctBy<fieldName>
:根据指定的单个条件进行去重查询;findFirstBy<fieldName>
:根据指定的单个条件进行等值查询(只返回满足条件的第一个数据);findBy<fieldName1>OrderBy<fieldName2>
:根据第一个字段做等值查询,并根据第二个字段做排序;......
:
大家看下来不难发现,SpringData-MongoDB
其实提供了一组命名约束,结合自定义的实体类字段,可以让SpringData
帮我们生成各类执行语句,这里总体归类一下,在Repository
中自定义方法,不同单词开头的含义如下:
- 以
get、find、read、query、stream
开头,代表是查询数据的方法; - 以
count
开头,代表是计数统计的方法; - 以
delete、remove
开头,代表是删除数据的方法; - 以
exists
开头,代表是判断是否存在的方法; - 以
search
开头,代表是全文搜索的方法; - 以
update
开头,代表是修改数据的方法;
上面这些便是SpringData-MongoDB
所有支持的方法类型,这些方法后面可以跟关键字,以find
开头的方法为例:
By
:表示当前方法生成的查询语句,会根据By
后面的逻辑来组成;FirstBy
:表示当前方法生成的语句,只会返回符合条件的第一条数据;DistinctBy
:表示当前方法生成的语句,会对符合条件的数据去重;TopBy
:表示当前方法生成的语句,只会返回符合条件的前N
条数据;<实体类名称>By
:表示当前方法生成的语句,只会返回一条数据;<实体类名称>sBy
:表示当前方法生成的语句,会返回多条数据;AllBy
:表示当前方法生成的语句,会返回多条或所有数据;DistinctFirstBy
:表示当前方法生成的语句,只会返回去重后的第一条数据;DistinctTopBy
:表示当前方法生成的语句,只会返回去重后的前N
条数据;
OK,在这些关键字之后,都是跟具体的字段名(实体类的属性名),字段名称后面可以接的关键字如下(同样以find
为例):
Or
:表示当前查询方法有多个条件,多个条件之间为"或者"关系;And
:表示当前查询方法有多个条件,多个条件之间为"并且"关系;OrderBy
:表示当前查询会涉及到排序,后面需要跟一个排序字段;Between
:表示当前方法为between
范围查询;GreaterThan
:表示当前方法为>
查询;GreaterThanEqual
:表示当前方法为>=
查询;LessThan
:表示当前方法为<
查询;LessThanEqual
:表示当前方法为<=
查询;After
:和GreaterThan
差不多,相当于查询指定数值之后的数据;Before
:和LessThan
差不多,查询指定条件之前的数据;Containing
:查询某字段中包含指定字符的数据;Empty
:表示当前方法会查询指定字段为空的数据,与之含义类似的还有Null、Exists
;Equals
:表示当前方法会根据指定字段做等值查询;Is
:和Equals
差不多;In
:表示当前方法为in
多值匹配查询;Like
:表示当前方法为like
模糊查询;Not
:可以和上述大多数关键字组合,带有Not
的则含义相反,如NotEmpty
表示不为空;......
上面列出了大多数常用的连接关键字,看着是不是尤其的多?但其实不需要你死记,因为当你在IDEA
工具编写方法时,它也会自动提示你!这里随便来定义几个方法,帮助诸位加深理解:
java
@Repository
public interface PandaRepository extends MongoRepository<Panda, Integer> {
// 查询指定年龄的熊猫数量
Integer countByAge(Integer age);
// 对熊猫姓名进行全模糊查询
List<Panda> findByNameLike(String keyword);
// 查询同时满足年龄、姓名两个条件的熊猫
Panda findPandaByAgeAndName(int age, String name);
// 查询满足颜色、年龄其中任一条件的所有熊猫
List<Panda> findByColorOrAge(String color, int age);
// 查询第一个带有颜色的熊猫
Panda findFirstByColorNotNull();
// 查询年龄大于等于指定岁数的熊猫
List<Panda> findByAgeGreaterThanEqual(int age);
// 对熊猫ID进行多值查询
List<Panda> findByIdIn(List<Integer> ids);
// 查询指定颜色的熊猫,并按照年龄降序返回
List<Panda> findByColorOrderByAgeDesc(String color);
// 查询年龄小于指定岁数的前三条数据
List<Panda> findTop3ByAgeLessThan(int age);
}
上面定义的方法就不挨个测试了,其实效果就如同注释中所说的一样,感兴趣的可以自行测试看看。
这里说个题外话,编程里有句话叫做:好的代码不用写注释 ,这点在SpringData
中体现的淋漓尽致,当你熟悉它这套命名约束后,再去看别人定义的Repository
方法时,就算他人不写注释,你也完全可以看懂,Why
?因为你们遵循着完全相同的编码规范、命名约束,他不可能写其他风格的代码,毕竟命名不按规范来,定义的方法压根无法执行~
2.3、分页查询
前面了解了SpringDataMongoDB
中的自定义方法,接着来看看经典的"分页查询",在这里该怎么玩呢?
首先需要定义一个Repository
方法,如下:
java
// 对年龄不为空的熊猫数据进行分页
Page<Panda> findByAgeNotNull(Pageable pageable);
命名约束完全相同,区别就在于返回类型,以及入参类型不同,这里的返回类型声明成了Page<Panda>
,而入参列表中需要新增一个Pageable
,接着继续封装一下Service
层:
java
// 在PandaService接口中定义分页方法
Page<Panda> pageByAgeNotNull(int pageNumber, int pageSize);
// 在PandaServiceImpl实现类中实现具体逻辑
@Override
public Page<Panda> pageByAgeNotNull(int pageNumber, int pageSize) {
// 使用SpringData中内置的分页请求对象,直接将分页参数转换为请求对象
PageRequest pageReq = PageRequest.of(pageNumber - 1, pageSize);
// 调用Repository的分页方法
return pandaRepository.findByAgeNotNull(pageReq);
}
写好Service
层后,为了测试分页功能是否正常,这里再编写一个单元测试:
java
@Test
void testPage(){
Page<Panda> pandas = pandaService.pageByAgeNotNull(1, 3);
int totalPages = pandas.getTotalPages();
long totalElements = pandas.getTotalElements();
System.out.println("总页数:" + totalPages);
System.out.println("总行数:" + totalElements);
System.out.println("第一页的数据为:");
List<Panda> data = pandas.getContent();
// 也可以直接遍历pandas这个Page对象
data.forEach(System.out::println);
}
这个用例很简单,无非就是调用了一下service
方法,而后传递了页码、条数,接着从Page
对象中,拿到了总页数、总行数,以及具体的分页数据,输出结果如下:
java
总页数:6
总行数:16
第一页的数据为:
Panda(id=1, name=肥肥, age=3, color=黑白色, food=null)
Panda(id=4, name=黑熊, age=3, color=黑白色, food=Food(name=黄金竹, grade=S))
Panda(id=5, name=白熊, age=4, color=null, food=Food(name=翠绿竹, grade=B))
效果很明显,总共有16
条数据,每页三条总共6
页,这里获取了第一页的数据,拿到的分页结果也符合age
字段不为空的特性,当然,大家也可以自行改改页码测试,这里不再重复贴结果,毕竟分页使用起来尤为简单。
2.4、自定义MongoDB语句
前面掌握了遵循SpringData
的命名规范,通过自定义Repository
方法,完成了对MongoDB
数据的增删改查操作,但这种方式有个缺陷,就是必须遵守规范去给方法命名,使用时难免会有种约束感,能不能自由一点呢?当然可以。
以前操作传统关系型数据库时,无论是在任何ORM
框架中,都支持通过注解来编写简单SQL
,例如MyBatis
:
java
@Select("select * from zhu_zi where id = #{id}")
ZhuZi selectById(@Param("id") Integer id);
那在SpringDataMongoDB
中,支不支持这种注解形式定义语句呢?答案是当然可以,来看个例子:
java
// Repository方法定义
@Query("{'name': ?0}")
List<Panda> queryXxx(String name);
// Service接口定义
List<Panda> queryXxx(String name);
// ServiceImpl接口实现
@Override
public List<Panda> queryXxx(String name) {
return pandaRepository.queryXxx(name);
}
这个代码特别简单,重点看repository
里定义的接口方法:queryXxx()
,在方法上面有一个@Query
注解,这用于自定义查询语句,其中声明根据name
字段进行查询,?0
表示方法参数的索引(占位符),此处的0
表示第一个参数name
,下面看看测试结果:
java
// Junit测试用例
@Test
void testQueryXxx() {
List<Panda> pandas = pandaService.queryXxx("肥肥");
pandas.forEach(System.out::println);
}
/* 执行结果:
Panda(id=1, name=肥肥, age=3, color=粉色, food=null)
*/
这里并没有遵循SpringData
的命名规范,但仍然根据name
字段查询到了数据,效果同预期中的一样,除此之外,还有另外几个注解,分别对应其他操作:
@Update
:用于自定义更新语句的注解;@DeleteQuery
:用于自定义删除语句的注解;@CountQuery
:用于自定义统计语句的注解;@ExistsQuery
:用于自定义查询语句,但执行后只返回是否存在满足条件的数据,并不返回具体的文档;@Aggregation
:用于自定义聚合管道语句的注解;......
除开上述列出的外,其实还有一些注解,具体大家可以去看看org.springframework.data.mongodb.repository
这个包,关于这些注解的用法就不再过多说明,毕竟用注解写原生语句的机会并不多,实际开发中,要么根据命名规范自定义方法,要么使用MongoTemplate
写复杂操作(后续会细说)。
2.5、SpingDataMongoDB事务机制
在之前的篇章中提到过,MongoDB
本身支持事务,那SpingDataMongoDB
中该如何使用事务呢?如下:
java
// Service层定义接口
void mongoTransaction();
// ServiceImpl具体实现
@Override
public void mongoTransaction() {
// 1.先通过mongoClient开启一个session会话
ClientSession session = mongoClient.startSession();
try{
// 2.通过session开启事务
session.startTransaction();
// 3.创建一个实体对象
Panda panda = new Panda();
panda.setId(222);
panda.setName("白白");
panda.setColor("白色");
panda.setAge(1);
// 4.通过mongoClient获取集合对象
MongoCollection<Document> collection = mongoClient
.getDatabase("zhuzi")
.getCollection("xiong_mao");
// 5.通过集合对象提供的insert方法插入数据
collection.insertOne(session, Document.parse(JSONObject.toJSONString(panda)));
// 6.模拟执行异常
int n = 100 / 0;
// 7.如果执行到这里,说明执行没报错,提交事务
session.commitTransaction();
} catch (Exception e) {
// 8.如果进入了catch,说明出现异常,回滚事务
session.abortTransaction();
e.printStackTrace();
}
// 9.关闭前面开启的session会话
session.close();
}
OK,对于每一步代码的含义,这里已经在上面写出了注释,为此不再重复说明,整个流程很简单,上面是一个向MongoDB
插入单个文档的方法,如果执行成功,提交事务;如果执行出现异常,回滚事务,但要注意几点:
- ①事务依赖于
ClientSession
开启,所以需要先打开ClientSession
会话; - ②想要让当前写操作的事务生效,必须调用
MongoCollection
的写方法; - ③前两个对象的获取,都依赖于
MongoClient
对象,所以要记得注入该对象,如下:
java
@Autowired
private MongoClient mongoClient;
接着来写个测试用例,看看事务到底会不会生效呢?如下:
java
@Test
void testMongoTransaction() {
pandaService.mongoTransaction();
}
按理来说,由于咱们手动加了一行100/0
,因此肯定会报错而后回滚,但是来看看实际执行结果:
java
Caused by: com.mongodb.MongoCommandException:
Command failed with error 20 (IllegalOperation):
'Transaction numbers are only allowed on a replica set member
or mongos' on server 192.168.229.136:27017. The full response is
{"ok": 0.0, "errmsg": "Transaction numbers are only allowed on a
replica set member or mongos", "code": 20, "codeName": "IllegalOperation"}
这个报错很明显,并不是取模零的异常,而是告诉咱们,事务只能在replicaSet
副本集,或者mongos
分片集群下生效,在单机的MongoDB
上,事务无法正常开启,为此,在使用单机MongoDB
时,事务并不能正常生效(如果你搭建了集群环境,上述代码是能正常执行、然后回滚数据的)。
三、MongoTemplate
前面的内容中,简单过了一下SpringData
自定义方法、如何定义分页查询,以及如何通过注解自定义原生语句。不过更多时候,咱们都是在遵循着SpringData
的命名规范编写,语句的生成、执行、数据聚合,全由SpringData
自动完成。但,如果有一个特殊的需求,无法通过前面的方式查询到数据怎么办?这里就得用到MongoTemplate
,可以用它来实现更加灵活的增删改查操作。
3.1、MongoTemplate方法分类
MongoTemplate
中提供了不同的方法,用于执行不同类型的操作,首先来看看插入方法:
java
// 插入一个文档(自动根据对象类型解析目标集合)
T insert(T objectToSave);
// 向指定的集合插入一个文档
T insert(T objectToSave, String collectionName);
// 批量插入多个文档
Collection<T> insertAll(Collection<? extends T> objectsToSave);
// 向指定的集合里批量插入多个文档
Collection<T> insert(Collection<? extends T> batchToSave, String collectionName);
Collection<T> insert(Collection<? extends T> batchToSave, Class<?> entityClass);
// 批量插入多个文档,并设置写关注级别
Collection<T> doInsertBatch(String collectionName, Collection<? extends T> batchToSave,
MongoWriter<T> writer);
// 这两个方法等价于前面的insert方法
T save(T objectToSave);
T save(T objectToSave, String collectionName);
观察下来会发现,这些方法对应着MongoDB
自身提供的API
,即insertOne、insertMany
,如果入参T
已经与一个集合映射,则无需额外指定类型、集合名,MongoTemplate
会自动解析。但如果你想将一个字段完全不同的文档插入到某个集合,则可以显式指定集合名称。
接着来看看删除方法:
java
// 删除一个文档(根据入参类型自动解析目标集合)
DeleteResult remove(Object object);
// 从指定集合中删除一个文档
DeleteResult remove(Object object, String collectionName);
// 根据条件从指定集合中删除一个文档
DeleteResult remove(Query query, Class<?> entityClass);
DeleteResult remove(Query query, Class<?> entityClass, String collectionName);
DeleteResult remove(Query query, String collectionName);
// 根据指定条件删除文档,并返回删除前的文档(默认只对第一个满足条件的文档生效)
<T> T findAndRemove(Query query, Class<T> entityClass);
删除方法命名中都带remove
,可以直接传入一个对象进行删除,也可以根据条件进行删除,没啥好讲的,接着看看修改方法:
Java
// 更新指定的文档
ExecutableUpdate<T> update(Class<T> domainType);
// 更新满足条件的第一个文档(如果未找到满足条件的文档,则将当前数据插入到集合)
UpdateResult upsert(Query query, UpdateDefinition update, Class<?> entityClass);
// 更新满足条件的第一个文档
UpdateResult updateFirst(Query query, UpdateDefinition update, String collectionName);
// 更新满足条件的所有文档
UpdateResult updateMulti(Query query, UpdateDefinition update, Class<?> entityClass);
// 根据指定条件删除文档,并返回删除前的文档(默认只对第一个满足条件的文档生效)
T findAndModify(Query query, UpdateDefinition update, Class<T> entityClass);
// 根据指定条件替换文档,并返回删除前的文档(默认只对第一个满足条件的文档生效)
<S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions options,
Class<S> entityType, String collectionName, Class<T> resultType);
修改方法有点多,所以这里省略了一些同名不同参的方法,其中只有updateMulti
方法会更新所有满足条件的文档,其余方法都只会更新"满足条件"的第一个文档。为此,在使用时要额外注意:当你的操作涉及到多个文档修改时,请记住调用updateMulti()
方法,而不是其他修改方法。
好了,关于这些方法如何使用放到后面说,再来看看查询方法,这也是MongoTemplate
中最多的一类:
java
// 查询指定集合里是否存在满足条件的数据
boolean exists(Query query, String collectionName);
// 根据ID查询数据
T findById(Object id, Class<T> entityClass);
// 返回满足查询条件的单条数据
T findOne(Query query, Class<T> entityClass);
// 返回满足查询条件的多条数据
List<T> find(Query query, Class<T> entityClass);
// 查询指定集合中的所有文档
List<T> findAll(Class<T> entityClass, String collectionName);
// 返回满足条件的所有数据(并根据指定字段去重)
List<T> findDistinct(Query query, String field, Class<?> entityClass, Class<T> resultClass);
// 统计指定文档中,满足查询条件的文档数量
long count(Query query, String collectionName);
// 使用聚合管道查询数据
AggregationResults<O> aggregate(TypedAggregation<?> aggregation, Class<O> outputType);
上面同样省略了一些同名的重载方法,以及某些不常用的方法,诸位可以简单瞟几眼。除开上述增删改查四类方法外,MongoTemplate
还提供了执行聚合管道命令的方法,如下:
java
// 在指定集合中,执行aggregate聚合管道操作
AggregationResults<O> aggregate(Aggregation aggregation,
String collectionName, Class<O> outputType)
// 支持异步Flux流(响应式编程)的聚合管道操作
CloseableIterator<O> aggregateStream(Aggregation aggregation,
String collectionName, Class<O> outputType)
// 执行给定的aggregate聚合管道操作(这种方式无需传入集合名,而是采用Java映射类的形式)
AggregationResults<O> aggregate(TypedAggregation<?> aggregation, Class<O> outputType);
这里同样省略了一些重载方法,不过这并不重要,执行聚合管道操作的方法分为两大类,一类是同步执行的aggregate
方法,即命令发给MongoDB
服务端后,需要阻塞等待至MongoDB
返回结果为止;而另一类是支持响应式编程的aggregateStream
方法,可以通过调用subscribe()
方法或其他响应式编程操作来处理聚合结果(后面再细说)。
前面对MongoTemplate
的常用方法有了一定认识后,下面来说说如何使用MongoTemplate
。
3.2、Query与Criteria对象
在上一节中,大家会看到许多入参类型为Query
的方法,这到底是个啥东东?
用过MyBatisPlus
框架的小伙伴一定不陌生,在MP
中查询、修改、删除数据时,如果不想编写xml
文件,咱们可以通过Warpper
条件构造器,以Java
代码的形式指定where
条件,从而快捷、方便的实现条件修改、查询、删除功能。
而SpringData-MongoDB
,Query、Criteria
对象的作用亦是如此,通过Java
对象的方式,完全取代掉MongoDB
原生的shell
语法,比如你想实现一个条件查询,可以这样写:
java
// Servcie接口方法定义
List<Panda> findByColor(String color);
// ServiceImpl中的具体实现
public List<Panda> findByColor(String color) {
Query query = new Query();
query.addCriteria(Criteria.where("color").is(color));
return mongoTemplate.find(query, Panda.class);
}
这样就实现了按照颜色字段来查询数据的效果,当然,上面的查询条件还可以简写为:
java
Query query = Query.query(Criteria.where("color").is(color));
这样指定的条件,效果和前面new
的方式相同,来看看效果:
java
@Test
void testFindByColor(){
List<Panda> pandas = pandaService.findByColor("黑白色");
pandas.forEach(System.out::println);
}
/* 输出结果如下:
Panda(id=1, name=肥肥, age=3, color=黑白色, food=null)
Panda(id=2, name=花花, age=null, color=黑白色, food=null)
Panda(id=4, name=黑熊, age=3, color=黑白色, food=Food(name=黄金竹, grade=S))
*/
从结果中可以看出,和我们预期中的效果相同,的确实现了按颜色查询集合文档的效果。
3.2.1、Query查询对象详解
经过上述小案例的学习后,各位应该大致对SpringData
中的条件构造器有了基本认知,接着来详细说说Query
对象,各方法的作用如下:
java
// 定义条件查询对象
Query.query(CriteriaDefinition criteriaDefinition);
// 在现有的Query对象上,新增一个条件对象
addCriteria(CriteriaDefinition criteriaDefinition);
// 在现有查询对象上,新增一个Sort排序对象
with(Sort sort);
// 在现有查询对象上,新增一个Pageable分页对象
with(Pageable pageable);
// 查询结束后,跳过指定的前skip条数据
skip(long skip);
// 查询结束后,限制返回的行数
limit(int limit);
// 查询结束后,限制返回的字段(投影查询)
Field fields();
好了,Query
类中实则定义了几十个方法,但较为常用的则是上面列出的这几个,通常咱们理解上面这几个方法的作用即可,下面结合前面的案例,简单过一下这些方法:
java
// 一、定义Service接口方法
List<Panda> findByColorAndAge(String color, int age);
// 二、编写Impl子类实现
@Override
public List<Panda> findByColorAndAge(String color, int age) {
// 1. 通过query()方法快速定义带条件的查询对象
Query query = Query.query(Criteria.where("color").is(color));
// 2. 通过addCriteria()方法,在现有的查询对象上新增一个条件
query.addCriteria(Criteria.where("age").is(age));
// 3. 通过with()方法添加一个排序对象(基于_id降序)
query.with(Sort.by(Sort.Direction.DESC, "_id"));
// 4. 通过skip()方法跳过结果集中的第一条数据
query.skip(1);
// 5. 通过limit()方法限制返回的行数为一条
query.limit(1);
// 6. 通过fields()方法指定返回的字段
query.fields().include("_id", "name", "age");
return mongoTemplate.find(query, Panda.class);
}
// 三、撰写单元测试用例
@Test
void testFindByColorAndAge(){
List<Panda> pandas = pandaService.findByColorAndAge("黑白色", 3);
pandas.forEach(System.out::println);
}
这个案例中,几乎将前面列出的方法都用上了,接着来看看结果:
java
/* 集合内符合查询条件的数据如下:
Panda(id=1, name=肥肥, age=3, color=黑白色, food=null)
Panda(id=4, name=黑熊, age=3, color=黑白色, food=Food(name=黄金竹, grade=S))
*/
/* 输出结果如下:
Panda(id=1, name=肥肥, age=3, color=null, food=null)
*/
来根据代码推导一下过程,首先基于给定的条件,会找到id=1、4
这两条数据;其次会基于id
降序,结果集变为4、1
这个顺序;然后回通过skip
跳过第一条数据,即id=4
这条数据;接着通过limit
限制返回一条数据,拿到id=1
这条数据;最后通过fields
指定返回的字段,因为没有要求返回color、food
,所以最终结果集相应字段为null
。
好了,这个结果和咱们预期中的完全相同,不过上面这些案例中,又涉及到了一些新对象,如Sort、Field
等,这些对象也会有相应API
,这里简单过一下,先来看看Sort
类:
java
// 仅有的构造器,使用private关键字修饰
private Sort(Direction direction, List<String> properties);
// 对外提供by()方法来创建实例对象
public static Sort by(String... properties);
public static Sort by(List<Order> orders);
public static Sort by(Order... orders);
public static Sort by(Direction direction, String... properties);
// 快速创建升序、降序对象的方法
public Sort descending();
public Sort ascending();
// 默认的排序方式(升序)
public static final Direction DEFAULT_DIRECTION;
static {
DEFAULT_DIRECTION = Sort.Direction.ASC;
}
这里咱们重点关注by()
方法即可,排序主要有两个入参,一个是排序方式,另一个是排序字段,如果调用不指定排序方式的by
方法,默认使用DEFAULT_DIRECTION
升序方式。再者,我们可以直接指定这两个参数,也可以封装成Order
对象,Order
内部类如下:
java
public static class Order implements Serializable {
// 排序方式
private final Direction direction;
// 排序字段
private final String property;
// 使用默认的升序方式,为传入的字段构建Order对象
public static Order by(String property) {
return new Order(Sort.DEFAULT_DIRECTION, property);
}
// 使用升序方式为传入的字段构建Order对象
public static Order asc(String property) {
return new Order(Sort.Direction.ASC, property, DEFAULT_NULL_HANDLING);
}
// 使用降序方式为传入的字段构建Order对象
public static Order desc(String property) {
return new Order(Sort.Direction.DESC, property, DEFAULT_NULL_HANDLING);
}
// 判断一个Order对象是否为升序
public boolean isAscending() {
return this.direction.isAscending();
}
// 判断一个Order对象是否为降序
public boolean isDescending() {
return this.direction.isDescending();
}
// 省略其他成员与方法......
}
Order
类并不难懂,无非是对排序方式、排序字段的封装,用起来也差不多,只不过在使用时,需要先构建一个Order
对象,而后再传入到Sort
对象中,如下:
java
query.with(Sort.by(Sort.Order.desc("_id")));
好了,另一个Direction
类没啥好讲的,就是一个枚举类,里面就定义了ASC、DESC
两个枚举,下面来看看Filed
类,该类主要用于投影查询,即调用Query.fileds()
方法时产生的对象,如下:
java
// Query.fileds()方法的定义
public Field fields();
public class Field {
// 指定本次查询需要返回的字段
public Field include(String... fields);
// 指定本次查询需要排除的字段
public Field exclude(String... fields);
// 用于数组型字段,offset代表切片起始位置,size表示要获取的元素数量
public Field slice(String field, int offset, int size);
// 返回数组字段中,指定下标的元素值
public Field position(String field, int value);
// 用于再次过滤数组/嵌套文档中的数据
public Field elemMatch(String field, Criteria elemMatchCriteria);
// 省略其他不常用方法......
}
上面列出了Filed
类中的常用方法,这里主要关心前面两个方法,一个是指定需要返回的字段列表,另一个是指定需要排除的字段列表,如下:
java
// 指定返回name、age字段,并强制将_id字段排除
query.fields().include("name", "age").exclude("_id");
而对于后面的几个方法,都是用于数组/嵌套类型的字段,一般情况下使用较少,为此,这里不再举例说明,下面重点来看看Criteria
条件构造器对象。
3.2.2、Criteria条件对象详解
在MongoTemplate
中提供的大部分增删改方法,其中都会有一个Query
类型的入参,而Query
对象无法指定过滤条件,因此需要结合Criteria
对象来指定条件,大家可以把Criteria
理解成MyBatisPlus
框架的Warpper
对象,当然,它们两者在使用上也相差不大。
在前面的案例中,咱们曾写过这样的代码:
java
Query query = Query.query(Criteria.where("color").is(color));
query.addCriteria(Criteria.where("age").is(age));
其实这两者都是在指定过滤条件,最终都会转变成一个Criteria
对象传入到Query
对象中,下面来看看其中的方法:
java
// 过滤条件的起始方法(类似于SQL里的where关键字)
public static Criteria where(String key);
// 用于连接多个条件(and并且关系)
public Criteria and(String key);
// 匹配所有与指定值相等的文档(=)
public Criteria is(Object value);
// 匹配所有指定字段为空的文档(is null)
public Criteria isNull();
// 匹配所有不等于指定值的文档(!=)
public Criteria ne(@Nullable Object value);
// 匹配所有小于指定值的文档(<)
public Criteria lt(Object value);
// 匹配所有小于、等于指定值的文档(<=)
public Criteria lte(Object value);
// 匹配所有大于指定值的文档(>)
public Criteria gt(Object value);
// 匹配所有大于、等于指定值的文档(>=)
public Criteria lt(Object value);
// 匹配所有与指定的多个值、其中一个相等的文档(in)
public Criteria in(Object... values);
// 匹配所有与指定的多个值、不相等的文档(not in)
public Criteria nin(Object... values);
// 匹配所有与指定的多个值、完全相等的文档(用于数组字段)
public Criteria all(Object... values);
// 匹配指定字段是否存在的所有文档(true:存在,false:不存在)
public Criteria exists(boolean value);
// 可以与上面大多数方法配合使用,含义为取反
public Criteria not();
// 匹配所有符合正则表达式的文档(可用于实现like查询)
public Criteria regex(String regex);
// 连接多个条件的方法(or或者关系)
public Criteria orOperator(Criteria... criteria);
// 连接多个条件的方法(and并且关系)
public Criteria andOperator(Criteria... criteria);
// 连接多个条件的方法(nor非或关系)
public Criteria norOperator(Criteria... criteria);
// 省略其他方法.......
上面列出了Criteria
类中大多数常用方法,其他一些要么属于操作嵌套文档、地理空间的方法,要么就比较小众冷门的方法,所以不再详细罗列。其实大家观察这些方法的命令,应该也能摸清楚大致含义,无非就是原生语法,转变成了Java
代码形式,前面自定义Repository
方法也是相同的含义,将各种条件表达式,以"方法命名规范"的形式来编写。
毕竟这里提供的方法,和前面的命名规范有很多共通之处,因此就随便来个例子练习一下:
java
// 查询所有age>=3,并且food.grade=S、B的文档
Criteria criteria = Criteria.where("age")
.gte(3)
.and("food.grade")
.in("S", "B");
Query query = Query.query(criteria);
List<Panda> pandas = mongoTemplate.find(query, Panda.class);
pandas.forEach(System.out::println);
/* 输出结果如下:
Panda(id=5, name=白熊, age=4, color=null, food=Food(name=翠绿竹, grade=B))
Panda(id=4, name=黑熊, age=3, color=黑白色, food=Food(name=黄金竹, grade=S))
Panda(id=12, name=金熊, age=4, color=null, food=Food(name=黄金竹, grade=S))
* */
这个结果和代码没啥好说的,一眼就能看懂。不过这里注意:之前咱们写的例子,几乎每一步都是分开写的,但要注意,SpringDataMongoDB
中的大多数方法,都支持函数式编程的链式调用风格,即可以一直.
下去,不需要每行代码之间用;
分割。
3.2、Update修改对象
前面讲完了Query、Criteria
两个类之后,接着来详细说说Update
对象,大家对这玩意儿或许有点陌生,毕竟前面都没出现过,对不?其实该对象主要作用于"修改"方法,可仔细观察MongoTemplate
提供的修改方法,貌似入参都为UpdateDefinition
类型呀!这里大家可以仔细去看看Update
类的定义:
java
public class Update implements UpdateDefinition {}
从类头能明显看出,UpdateDefinition
是个接口,而Update
则是具体的实现类,所以到这里就清楚了,调用MongoTemplate
提供的修改方法时,其实我们需要传递的是Update
对象,这里先来个案例简单使用一下:
java
// Service接口中定义:根据ID修改年龄的方法
UpdateResult updateColorById(Integer id, String color);
// Impl类中的具体实现
@Override
public UpdateResult updateColorById(Integer id, String color) {
Query query = new Query(Criteria.where("id").is(id));
Update update = new Update().set("color", color);
return mongoTemplate.updateMulti(query, update, Panda.class);
}
// 测试用例
@Test
void testUpdateAgeById() {
Panda oldPanda = pandaService.findById(1);
System.out.println("修改前的数据:" + oldPanda);
UpdateResult result = pandaService.updateColorById(1, "粉色");
System.out.println("受影响的行数:" + result.getModifiedCount());
Panda newPanda = pandaService.findById(1);
System.out.println("修改前的数据:" + newPanda);
}
上面这个案例,代码也特别容易看明白,上面总共有两个对象:Query、Update
,其中Query
对象用于承载过滤条件,而Update
对象则用来承载要修改的数据,来看看输出结果:
java
/*
修改前的数据:Panda(id=1, name=肥肥, age=3, color=黑白色, food=null)
受影响的行数:1
修改前的数据:Panda(id=1, name=肥肥, age=3, color=粉色, food=null)
*/
从修改前后的数据对比来看,本次修改的确生效了,而且最关键的一点是:本次修改并没有触发全量替换,而是动态修改,即只变更了修改字段的值,其他字段没有变为Null
!这是因为MongoTemplate
的update
系列方法,底层使用了$set
操作符。同时注意,Update
系方法的返回值,都为UpdateResult
对象,该对象主要有四个常用方法:
getMatchedCount()
:获取满足条件的文档数;getModifiedCount()
:获取本次修改操作影响的行数;getUpsertedId()
:如果本次修改操作为upsert
操作,返回未匹配到数据、插入数据后生成的ID
;wasAcknowledged()
:判断本次MongoDB
服务端执行完成后,其返回值是否正确。
好了,大概认识了Update
对象的用法后,接着来看看其中提供的方法:
java
// 将指定字段的值,修改为给定值
public static Update update(String key, @Nullable Object value);
// 将指定字段的值,修改为给定值
public Update set(String key, @Nullable Object value);
// 将指定字段的值,修改为给定值(只有在指定字段为null的情况下生效)
public Update setOnInsert(String key, @Nullable Object value);
// 删除指定字段的值
public Update unset(String key);
// 对指定字段的值进行自增(前者代表+1,后者代表+inc)
public void inc(String key);
public Update inc(String key, Number inc);
// 向指定的字段,插入当前时间
public Update currentDate(String key);
// 向指定的字段,插入当前时间戳
public Update currentTimestamp(String key);
// 向指定的数组字段末尾,追加单个元素(可以插入重复值)
public Update push(String key, @Nullable Object value);
// 向指定的数组字段末尾,追加多个元素(可以插入重复值)
public Update pushAll(String key, Object[] values);
// 从指定的数组字段中,移除指定下标的元素
public Update pop(String key, Position pos);
// 从指定的数组字段中,移除单个数组元素
public Update pull(String key, @Nullable Object value);
// 从指定的数组字段中,移除多个数组元素
public Update pullAll(String key, Object[] values);
// 向数组中添加一个值,如果该值在数组中已存在,则忽略本次插入动作
public Update addToSet(String key, @Nullable Object value);
// 向数组中添加多个值,如果插入的某个值在数组中已存在,自动忽略相应值
public AddToSetBuilder addToSet(String key);
// 上述方法必须结合AddToSetBuilder.each方法使用,如addToSet("names").each(...)
public Update each(Object... values);
// 根据条件过滤数组内的元素(结合set("filed.$[element]", "...")方法一起使用)
public Update filterArray(CriteriaDefinition criteria);
// 将指定字段的名称,修改为新的字段名
public Update rename(String oldName, String newName);
OK,上面同样未曾将所有方法列出,仅仅只写了一些常用方法,其实如果你的集合中,不涉及到数组类型的字段,那只需要关注前半部分方法即可。当然,这里只说明了每个API
的作用,想练习的小伙伴可以自行去玩一玩,不会使用直接问ChatGPT
即可。
3.4、Aggregation聚合管道对象
在《MongoDB入门》这一篇文章中,曾经提到过一点,如果你想实现较为复杂的操作,几乎都需要通过聚合管道来完成,MongoDB
提供的聚合管道十分强大,几乎可以满足任何场景下的业务操作,而在Java
中如何使用聚合管道呢?答案是Aggregation
对象。
先从之前的篇章,cpoy
过来一个原生的聚合管道命令,如下:
JavaScript
/* 按年龄进行分组,并统计各组的数量(没有age字段的数据统计到一组) */
db.xiong_mao.aggregate([
// 1:通过$group基于age分组,通过$sum实现对各组+1的操作
{$group: {_id:"$age", count: {$sum:1}}},
// 2:基于前面的_id(原age字段)进行排序,1代表正序
{$sort: {_id:1}}
]);
这个案例比较简单,先基于年龄分组,再基于_id
字段排序,如何转换成Java
代码实现呢?如下:
Java
@Test
void testAggregation() {
Aggregation aggregation = Aggregation.newAggregation(
// 基于年龄字段分组,接着统计每组梳理,并为统计字段取别名
Aggregation.group("age").count().as("count"),
// 基于分组后的_id字段(原age)字段排序(升序)
Aggregation.sort(Sort.Direction.ASC, "_id")
);
AggregationResults<Map> results = mongoTemplate.aggregate(aggregation, "xiong_mao", Map.class);
List<Map> maps = results.getMappedResults();
maps.forEach(System.out::println);
}
/* 执行结果:
{_id=null, count=3}
{_id=1.0, count=3}
{_id=2.0, count=4}
{_id=3.0, count=6}
{_id=4.0, count=2}
{_id=9, count=1}
*/
这里为了节省篇幅,就不再写service
层代码了,直接在Test
方法中写逻辑,上面的代码大家参考注释即可,这里重点说说语法。认真阅读过《入门篇》的小伙伴应该还记得,MongoDB
的聚合管道,可以指定多个的阶段,在每个阶段中进行不同的处理,而上述代码中亦是如此,不过该如何指定不同的阶段呢?大家看到中间的,
了嘛?newAggregation()
方法可以传入任意个Aggregation
对象,每个对象则代表一个阶段。
同时,MongoDB
原生的语法中,并没有提供$count
统计操作符,想要实现计数,只能以$sum:1
这种方式实现,而SpringData
则封装了一个count()
方法提供给咱们使用,可谓是十分贴切~
接着看看返回结果:AggregationResults
对象,这个对象需要指定一个泛型,该泛型意味着返回结果的类型,我这里传入了Map
,这是因为我懒得再次封装一个对象出来,如果大家以后有些聚合操作要经常执行,并且返回结果的字段已经确定,最好的做法还是封装一个entity
类出来。这点先放一遍,先说说AggregationResults
里的常用方法:
getMappedResults()
:返回映射的聚合结果集,即得到指定类型的集合(如案例中的List<Map>
);getRawResults()
:返回未做映射处理的原始结果集,则获取MongoDB
返回的BSON
对象;getUniqueMappedResult()
:返回映射后的一个结果对象,在明确清楚聚合结果只有一条的情况使用;iterator()
:返回用于遍历聚合结果的迭代器,可以用于逐个处理聚合结果中的数据;
对于AggregationResults
对象而言,咱们只要明白这几个方法的作用即可,至于其他没列出的并不重要。
3.4.1、Aggregation案例练习
好了,上阶段对SpringData
聚合管道有了简单认知后,那聚合管道的其他方法呢?怎么使用?其实把MongoDB
原生的聚合管道命令掌握后,在Java
里的意思也是相同的,毕竟方法名字都没改,和操作符的名称一模一样,下面来些案例玩玩:
java
// 案例一:基于颜色分组,并求出每组中年龄最大值,最后按年龄降序排列
@Test
void testAggregation1() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.group("color").max("age").as("max_age"),
Aggregation.sort(Sort.Direction.DESC, "max_age")
);
AggregationResults<Map> results = mongoTemplate.aggregate(aggregation, "xiong_mao", Map.class);
Document bson = results.getRawResults();
System.out.println(bson.toJson());
}
/* 执行结果:
{
"results": [{
"_id": "黑色", "max_age": 9
}, {
"_id": null, "max_age": 4.0
}, {
"_id": "粉色", "max_age": 3.0
}, {
"_id": "黑白色", "max_age": 3.0
}],
"ok": 1.0
}
*/
这个案例不难,相信大家一定能看懂,这里顺便演示了一下获取原生BSON
对象,而后转换为JSON
字符串输出的过程。下面再来一个稍微复杂的案例看看:
java
/* 案例二:过滤掉food为空,以及age小于3岁的数据,接着按food.grade分组,
并求出每组的平均年龄、以及输出每组第一个、最后一个、所有熊猫姓名,
最后按照平均年龄升序排列 */
@Test
void testAggregation2() {
// 1.组装过滤条件,food字段必须存在,并且age大于等于3
Criteria criteria = Criteria.where("food").exists(true).and("age").gte(3);
Aggregation aggregation = Aggregation.newAggregation(
// 2.过滤阶段:传入构建好的条件对象
Aggregation.match(criteria),
// 3.分组阶段:按food.grade食物等级分组
Aggregation.group("food.grade")
// 3.1. 求出age字段的平均值
.avg("age").as("avg_age")
// 3.2.拿到每组的第一个熊猫姓名
.first("name").as("first_name")
// 3.3.拿到每组的最后一个熊猫姓名
.last("name").as("last_name")
// 3.4.将所有熊猫姓名push到names数组
.push("name").as("names"),
// 4.排序阶段:将分组后的数据,按平均年龄升序排列
Aggregation.sort(Sort.Direction.ASC, "avg_age")
);
AggregationResults<Map> results = mongoTemplate.aggregate(aggregation, "xiong_mao", Map.class);
List<Map> maps = results.getMappedResults();
maps.forEach(System.out::println);
}
/* 执行结果:
{_id=C, avg_age=3.0, first_name=粉熊, last_name=粉熊, names=[粉熊]}
{_id=S, avg_age=3.5, first_name=黑熊, last_name=金熊, names=[黑熊, 金熊]}
{_id=B, avg_age=4.0, first_name=白熊, last_name=白熊, names=[白熊]}
{_id=A, avg_age=5.0, first_name=棕熊, last_name=黑黑, names=[棕熊, 紫熊, 黑黑]}
*/
代码和结果,大家可以花个一分钟仔细看看,每行都写了注释,结果也的确达到了起初的需求,大家可以试着多理解、多练习,自然而然就掌握了如何去写,最后再来一个案例吧:
java
/* 案例三:先过滤掉food为空的数据,基于food.grade分组,且保留原文档,并输出分组字段值、
原文档的name、age字段、原文档在各分组中的下标,同时要支持分页功能 */
@Test
void testAggregation3() {
// 1.每页的数据量为2条
int pageSize = 2;
// 2.过滤掉food字段为空的条件对象
Criteria criteria = Criteria.where("food").exists(true);
// 3.用for循环模拟三个分页请求
for (int pageNumber = 1; pageNumber <= 3; pageNumber++) {
Aggregation aggregation = Aggregation.newAggregation(
// 4.过滤阶段:传入构建好的条件对象
Aggregation.match(criteria),
// 5.分组阶段:按food.grade分组,$$ROOT代表引用原文档,并放入pandas数组
Aggregation.group("food.grade").push("$$ROOT").as("pandas"),
// 6.拆分阶段:将前面每个分组的pandas数组拆成一个个文档(index:下标,true:防丢失)
Aggregation.unwind("pandas", "index", true),
// 7.投影阶段:去掉部分字段不显示,只显示_id(原food.grade)、name、age、index字段
Aggregation.project("_id","pandas.name","pandas.age","index"),
// 8.分页阶段:使用skip、limit来区分读取不同页的数据
Aggregation.skip((pageNumber - 1) * pageNumber),
Aggregation.limit(pageSize)
);
AggregationResults<Map> results = mongoTemplate.aggregate(aggregation, "xiong_mao", Map.class);
System.out.println("第" + pageNumber + "页数据为:");
List<Map> maps = results.getMappedResults();
maps.forEach(System.out::println);
}
}
/* 执行结果:
第1页数据为:
{_id=B, index=0, name=白熊, age=4.0}
{_id=A, index=0, name=棕熊, age=3.0}
第2页数据为:
{_id=A, index=1, name=紫熊, age=3.0}
{_id=A, index=2, name=黑黑, age=9}
第3页数据为:
{_id=S, index=1, name=英熊, age=2.0}
{_id=S, index=2, name=金熊, age=4.0}
*/
这个案例需求是我瞎编的,没有任何逻辑可言,目的在于帮大家熟悉复杂场景下的API
用法,代码的含义可参考注释,重点来说说执行结果,这个结果有些小伙伴可能看的有点懵,其实特别简单。_id
代表原本的food.grade
食物等级,而index
则是原文档在各分组中的下标,因为原文档按顺序被压入了pandas
数组,为啥会有重复值呢?
很简单,因为这个下标是以"分组"来区分的,上述B
分组只有一个文档,所以index=0
,A
分组中有三个文档,所以index=0、1、2
,其他分组以此类推......,这样解释后,相信大家一定能看懂这个执行结果。
到这里,关于SpringData
中如何使用聚合管道的案例,就暂且告一段落了,如果大家感觉还没有尽兴,可以自行回到《入门篇-聚合管道查询》阶段,将其中每个案例用Java
代码实现一遍~
3.5、ExampleMatcher匹配器对象
前面讲完了聚合管道,接着来说说匹配器,这是SpringData
家族都有的对象,用于在匹配查询条件时对查询对象进行更细粒度的匹配配置,我们可以通过它自定义匹配的行为,好比排除某些字段、只匹配某些字段等。
可能按上面这么说,估计大家也不太明白这玩意儿的作用,那么先上个简单例子,大家看完自然而然就明白了,如下:
java
@Test
void testExampleMatcher() {
Panda panda = new Panda();
panda.setName("黑黑");
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnoreCase()
.withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains());
Example<Panda> example = Example.of(panda, matcher);
Criteria criteria = new Criteria().alike(example);
List<Panda> pandas = mongoTemplate.find(Query.query(criteria), Panda.class);
pandas.forEach(System.out::println);
}
/* 执行结果:
Panda(id=111, name=黑黑, age=9, color=黑色, food=Food(name=黑晶竹, grade=A))
*/
上面是个使用ExampleMatcher
的案例,从结果来看,无非就是基于name
字段做查询,最终查到了"黑黑"这条数据,重点来看看匹配器的定义:
java
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnoreCase()
.withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains());
这是在定义匹配规则,withIgnoreCase()
表示忽略大小写,withMatcher()
表示具体的匹配逻辑,对于name
字段的条件,会以contains()
包含规则去匹配,大家可以尝试把案例中的"黑黑"替换为"黑",执行时照样能匹配到"黑黑"这条数据,因为"黑黑"包含了"黑"。
经过上述解释,相信诸位应该大致清楚ExampleMatcher
的作用了吧?它可以针对每个字段,自定义细粒度的匹配规则,ExampleMatcher
提供了三个方法用于创建匹配器:
java
// 创建一个默认的ExampleMatcher实例(底层调用了matchingAll()方法)
static ExampleMatcher matching();
// 创建一个匹配所有属性(字段)的ExampleMatcher实例
static ExampleMatcher matchingAll();
// 创建一个匹配任意属性(字段)的ExampleMatcher实例
static ExampleMatcher matchingAny();
下面再来看看ExampleMatcher
中,自定义匹配规则的方法:
java
// 为指定字段设置自定义的匹配规则
ExampleMatcher withMatcher(String propertyPath, GenericPropertyMatcher genericPropertyMatcher);
// 为字符串设置自定义的匹配规则
ExampleMatcher withStringMatcher(StringMatcher defaultStringMatcher);
// 设置匹配时忽略大小写
ExampleMatcher withIgnoreCase();
// 设置匹配时包含空值字段
ExampleMatcher withIncludeNullValues();
// 设置匹配时忽略空值字段
ExampleMatcher withIgnoreNullValues();
// 为指定字段设置"字段值转换器"
ExampleMatcher withTransformer(String propertyPath, PropertyValueTransformer
propertyValueTransformer);
// 设置匹配时要排除的字段值
ExampleMatcher withIgnorePaths(String... ignoredPaths);
OK,上面这些方法简单了解一下,看withMatcher()
方法的入参,是一个GenericPropertyMatcher
类型,而该类中提供了一些内置规则方法,比如前面的contains()
包含规则,其他方法如下:
java
// 设置匹配时,指定字段的值,必须包含给定值
public GenericPropertyMatcher contains();
// 设置匹配时,指定字段的值,必须以给定值开头
public GenericPropertyMatcher startsWith();
// 设置匹配时,指定字段的值,必须以给定值结尾
public GenericPropertyMatcher endsWith();
// 设置匹配时,指定字段的值,必须与给定值完全匹配
public GenericPropertyMatcher exact();
// 设置匹配时,指定字段的值,必须符合给定的正则表达式
public GenericPropertyMatcher regex();
// 设置匹配时,指定字段的值会区分大小写
public GenericPropertyMatcher caseSensitive();
// 设置匹配时,指定字段的值不区分大小写
public GenericPropertyMatcher ignoreCase();
上面便是常用的匹配规则,了解这些后,再回到前面的案例加深理解:
java
// 1.创建了一个对象,用于承载每个字段的条件值
Panda panda = new Panda();
panda.setName("黑");
// 2.创建了一个匹配器
ExampleMatcher matcher = ExampleMatcher.matching()
// 3.匹配规则一:忽略大小写
.withIgnoreCase()
// 4.匹配规则二:name字段必须包含前面指定的"黑"字
.withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains());
// 5.创建出匹配器实例
Example<Panda> example = Example.of(panda, matcher);
// 6.将其传入到条件对象中
Criteria criteria = new Criteria().alike(example);
// 7.使用mongoTemplate查询数据,将带有匹配器的条件对象传入
List<Panda> pandas = mongoTemplate.find(Query.query(criteria), Panda.class);
// 8.打印查询后的结果
pandas.forEach(System.out::println);
加上这些注释后,大家是不是就能很好理解了?其实整体很简单,就是一种"另类"的条件查询,可以针对每个字段指定多条匹配规则。
好了,关于这玩意儿就此打住,其实ExampleMatcher
用的也不是特别多,大家在这里了解即可,因为前面讲到的其他查询方式,足以满足日常开发中的所有需求,匹配器只有在某些特殊的业务场景下,才会使用。
四、MongoDB配置详解
PS:前面将
SpringDataMongoDB
的大多数操作都已经阐述完毕,还剩下一些配置解析、特殊场景下的解决方案没有写完,而要写的大体内容已经列出,由于目前时间不多,所以过两天再来补齐(这里先发出来,卡个金石活动的奖励,毕竟前面的内容已经有三万多字啦)~
4.1、副本集配置
4.2、分片集群配置
4.3、库内分表方案
4.4、多数据源整合
五、SpringData-MongoDB篇总结
- ①基于
MongoRepository
提供的命名约束,自定义方法实现基本增删改查; - ②基于注解形式,编写原生语句实现特定的增删改查;
- ③基于
MongoTemplate
编写定制化的增删改查; - ④基于
ExampleMatcher
进行复杂的增删改查;