😶🌫️ SpringBoot中MongoDB的骚操作用法
不知道大家在工作项目中有没有使用MongoDB,在哪些场景中使用。MongoDB作为NoSQL数据库,不像SQL数据库那样,可以使用Mybatis框架。
如果需要在SpringBoot中使用MongoDB的话,我目前知道有三种方式,第一种是直接使用MongoDB官方的SDK,第二种是使用SpringJpa的方式,第三种是使用MongoTemplate。第二种在内部也是使用MongoTemplate的方式,只是封装了一些通用的CRUD操作,MongoTemplate也是对官方SDK的操作封装,其实本质上是没有什么区别的。
我在工作项目中,在云存储和IM系统中都使用了MongoDB,MongoTemplate和SpringJpa都有使用过,但是SpringJpa并不是特别好用,同时也踩过很多的坑,下面就来看看MongoDB在SpringBoot中的高级用法。

公众号:后端随笔
MongoDB注解
Spring Data MongoDB提供了很多的注解来简化简化操作,这些注解包括@Id, @Document, @Field等,这些注解可以在org.springframework.data.annotation 和org.springframework.data.mongodb.core.mapping 包中找到。这些注解用于指示SpringBoot如何将Java对象映射到MongoDB的Document中。
@Id:该注解用于指定哪个字段被作为主键,可以配合@Field字段使用
@Id
@Field(value = "_id", targetType = FieldType.STRING)
private String userId;// 将userId字段作为主键, 存储到Mongodb中的字段名为_id
@Field:该注解用于指定Document中字段的名称,默认情况下,Spring会将Java对象的字段的名作为Document中的字段名,如果你希望Document中的字段名和Java对象中的字段名不同,那么可以使用该注解进行指定。
@Document:用于将一个Java类映射到MongoDB的集合,默认情况下,Spring使用类名作为Collection名字,但是你也可以使用该注解来自定义Collection名字。
监听器
使用MongoTemplate进行CRUD操作时,会触发多个不同种类的监听器,我们可以创建不同类型的监听器,从而对查询条件,删除条件,Document映射等进行修改,日志记录,性能优化等。

上面这7个监听器,全部由org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener#onApplicationEvent 方法触发,创建监听器也非常简单,只需要创建一个类继承自AbstractMongoEventListener ,然后根据所执行的CRUD操作,重写对应的方法,最后将该类放入Spring容器中就可以了,可以存在多个监听器。下面是监听器的一些基本用法:
设置主键值
MongoDB在插入时,如果没有指定_id字段的值,那么MongoDB会自动生成一个ObjectId类型的值作为_id 字段值,但是默认生成的是String类型。如果我们需要使用int,long类型作为_id字段类型,那么就必须在执行最终插入前手动进行设置。
假如又不想每次执行insert操作时,都手动设置对象中主键字段的值,那么可以在xcye.xyz.mongodb.demos.test.TestAbstractMongoEventListener#onBeforeConvert 方法中统一的对Java对象中主键字段进行赋值,比如使用uuid,雪花算法等自动生成一个唯一的主键值。
@Override
public void onBeforeConvert(BeforeConvertEvent<Object> event) {
Object source = event.getSource();
if (!(source instanceof MongoBaseDomain)) {
return;
}
MongoBaseDomain<?> mongoBaseDomain = (MongoBaseDomain<?>) source;
if (mongoBaseDomain.getId() != null) {
return;
}
// 根据id字段的类型,如Long,String,Integer,自动生成一个唯一的主键值
mongoBaseDomain.setId(idValue);
}
日志记录
onBeforeSave,onBeforeDelete 方法会在执行remove和save之前触发,我们可以分别在这两个方法中记录删除条件和最终保存的对象,对于update方法,我并没有找到任何的方法。
在Mybatis中可以记录执行的SQL,在MongoTemplate中,我们也可以通过该监听器来实现。但是需要注意的是,MongoTemplate中提供的触发方法只有7个,如果执行的是aggregate,bulk等操作,无法通过监听器来记录最终执行的操作语句。
移除_class
默认情况下,在将Java对象保存至MongoDB时,MongoTemplate会在Java对象转换为Document时,会增加一个额外的_class 字段用于保存该Java对象的全限定名。
在执行查询操作时,MongoTemplate也会在查询条件上增加{_class: {$in: [java全限定名,以及子类的全限定名]}}。需要注意的是,额外的增加查询条件和原始的条件是and 操作,正常情况下是没有任何问题的,但是如果我们在插入时,使用Map作为插入的对象,手动指定CollectionName,那么MongoTemplate不会在Document上增加_class 字段(MongoTemplate对Map不做任何的处理,Document本身就是Map的子类)。
在这种情况下,我们执行查询条件时(根据条件修改,删除,查询),可能会出现查询不到的情况,根本原因便是使用Map插入的这个Document上并没有_class 字段。
解决方法有两个:1. 移除_class,2. 对于使用Map插入时,手动设置Map对象中_class 字段的值,这两种方式各有优点。
我更倾向于移除_class。如果Java对象的全限定名称比较长,并且Collection中数据比较多时,每次保存时都设置_class ,势必会导致不必要的存储空间浪费,而且_class 的作用只是通知Spring,MongoDB中保存的这条Document需要被反序列化为哪个Java类。
正常情况下,我们并不会在同一个Collection中存储多个不同的Java类型,所以在每个Document中存储_class 是完全没有必要的。
从Document中移除_class后,反序列化还正常么?
确定Document应该反序列化为哪个Java对象的工作是在org.springframework.data.convert.DefaultTypeMapper#readType(S, org.springframework.data.util.TypeInformation<T>) 方法中进行的,默认的行为是从查询到的Document中获取_class 字段的值,然后和find(Query query, Class<T> entityClass) 中的entityClass进行比较,最终决定是使用Document中的_class 还是entityClass。
我在上面也说了,通常情 况下,我们并不会在同一个Collection中保存多个不同的Java对象,所以可以直接使用entityClass作为反序列化类型就可以了。
/**
* 根据source和basicType(source来自于数据库数据),返回一个更具体的类型信息, 默认行为为,从source中获取_class,并且根据全限定名从缓存中获取,
* 因为类型都是直接从mongoTemplate指定的,所以从{@link TypeInformation#getType()}中获取的Class便是最具体的类型
*
* @param source must not be {@literal null}.
* @param basicType must not be {@literal null}.
* @return 类型信息
*/
@Override
public <T> TypeInformation<? extends T> readType(Bson source, TypeInformation<T> basicType) {
Class<T> entityClass = basicType.getType();
// 如果entityClass为空, 将执行逻辑交给父类去处理,由或者可以从本地缓存中根据CollectionName获取Collection对应的Java类的
if (entityClass == null) {
return super.readType(source, basicType);
}
ClassTypeInformation<?> targetType = ClassTypeInformation.from(entityClass);
return basicType.specialize(targetType);
}
写操作
默认情况下,会在org.springframework.data.convert.DefaultTypeMapper#writeType(org.springframework.data.util.TypeInformation<?>, S) 方法中向Document中增加_class 字段,我们需要移除_class 字段,只需要让该方法什么都不做就行
/**
* 默认行为是在写操作时,向document中增加{_class: "全限定名"}
*
* @param info must not be {@literal null}.
* @param sink must not be {@literal null}.
*/
@Override
public void writeType(TypeInformation<?> info, Bson sink) {}
查询
默认情况下,会在writeTypeRestrictions(Document result, @Nullable Set<Class<?>> restrictedTypes) 方法中向查询条件中添加{_class: {$in:[]}},这会导致在没有_class 字段时,查询出错,解决方案也是重写writeTypeRestrictions 方法,让它什么都不做
/**
* 默认行文是在查询的时候,向语句中写入{_class: {$in: []}}
*
* @param result must not be {@literal null}
* @param restrictedTypes must not be {@literal null}
*/
@Override
public void writeTypeRestrictions(Document result, Set<Class<?>> restrictedTypes) {}
主键
在MongoDB中,主键字段名是固定的_id,默认情况下,如果在插入时,没有指定主键字段的值,那么MongoDB会自动生成一个ObjectId类型的值作为_id的值。
使用MongoTemplate执行insert操作时,也可以像Mybatis那样,如果对象中主键值缺失,那么保存成功后,MongoTemplate会将MongoDB自动生成的_id 值赋值给Java对象中@Id 注解修饰的字段值。
User user = new User();
user.setUsername("xcye");
user.setPassword("xcye");
User insert = mongoTemplate.insert(user);
// insert.id = xxxx
如果需要MongoTemplate自动设置id字段的值,必须保证id字段的类型是ObjectId, String,BigInteger ,否则在插入时,会抛出异常,具体判断方法请看org.springframework.data.mongodb.core.EntityOperations.MappedEntity#assertUpdateableIdIfNotSet。
自定义_id转换器
这是一个坑,假如User这个Collection中,使用userId作为_id 字段的值,这是一个字符串。当我们通过userId查询,修改,删除,可能会出现查询不到对应记录的情况,但是我们传入的userId确是真实存在的,而且这种情况只存在于部分userId中。
出现这种情况的原因是因为,MongoTemplate在执行时,会对传入的_id字段进行推断,其会判断传入的这个_id 是否是ObjectId类型,如果能转成ObjectId的话,那么MongoTemplate会使用ObjectId对象作为_id 的值,但是因为MongoDB中_id 字段的类型是普通的字符串,并非是ObjectId,所以就会出现查询不到的情况。
| _id(对应于Java对象中的userId): String | username: String | password: String |
|---|---|---|
| 66aeeb73142fcf1d5591c29c | xcye | 123456 |
我们传入的查询条件:
db.User.find({_id: "66aeeb73142fcf1d5591c29c"})
MongoTemplate执行时,推断出66aeeb73142fcf1d5591c29c能够转为ObjectId类型,于是最终的查询条件变为:
db.User.find({_id: new ObjectId("66aeeb73142fcf1d5591c29c")})
这个过程是在MongoConverter#convertId 方法中完成的
default Object convertId(@Nullable Object id, Class<?> targetType) {
if (id == null || ClassUtils.isAssignableValue(targetType, id)) {
return id;
}
// Spring推断出66aeeb73142fcf1d5591c29c能够转为ObjectId,于是targetType为ObjectId
if (ClassUtils.isAssignable(ObjectId.class, targetType)) {
if (id instanceof String) {
// 字符串被转为了ObjectId
if (ObjectId.isValid(id.toString())) {
return new ObjectId(id.toString());
}
// avoid ConversionException as convertToMongoType will return String anyways.
return id;
}
}
try {
return getConversionService().canConvert(id.getClass(), targetType)
? getConversionService().convert(id, targetType)
: convertToMongoType(id, (TypeInformation<?>) null);
} catch (ConversionException o_O) {
return convertToMongoType(id,(TypeInformation<?>) null);
}
}
所以为了避免普通的字符串被转为ObjectId,我们需要重写convertId方法。只需要创建一个类继承自MappingMongoConverter类,并且重写其中的convertId就可以了。
@AutoConfiguration(after = MongoAutoConfiguration.class, before = MongoDataAutoConfiguration.class)
@ConditionalOnSingleCandidate(MongoClient.class)
public class MongoAutoConfiguration {
@Bean
@ConditionalOnMissingBean(MongoConverter.class)
MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory factory, MongoMappingContext context,
MongoCustomConversions conversions) {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
//
MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, context) {
@Override
public Object convertId(Object id, Class<?> targetType) {
if (id == null) {
return null;
}
if (id instanceof String) {
return id;
}
// 其他的转换
}
};
mappingConverter.setCustomConversions(conversions);
return mappingConverter;
}
}
数据库自动切换
使用MongoTemplate操作时,我们可以动态的切换MongoDB数据库,这个功能在分库的场景下非常好用,动态切换MongoDB数据库是通过MongoDatabaseFactorySupport 来完成的。
MongoTemplate在每次执行时,都会调用org.springframework.data.mongodb.core.MongoTemplate#doGetDatabase 获取操作的数据库,我们只需要创建自己的MongoDatabaseFactory,在getMongoDatabase方法中返回操作的数据库就行,可以参照SimpleMongoClientDatabaseFactory。
@AutoConfiguration(after = MongoAutoConfiguration.class, before = MongoDataAutoConfiguration.class)
@ConditionalOnSingleCandidate(MongoClient.class)
public class MongoAutoConfiguration {
@Bean
MongoDatabaseFactorySupport<?> mongoDatabaseFactory(MongoClient mongoClient, MongoProperties properties) {
return new CustomMongoDatabaseFactory(mongoClient, properties.getMongoClientDatabase());
}
}
因为MongoDB是NoSQL数据库,在操作时,并不需要像SQL数据库那样,必须要数据库和数据库表存在才可以。MongoDB执行时,如果数据库或Collection不存在,那么其会自动创建。

😶🌫️ SpringBoot中MongoDB的骚操作用法的更多相关文章
- 实例讲解Springboot整合MongoDB进行CRUD操作的两种方式
1 简介 Springboot是最简单的使用Spring的方式,而MongoDB是最流行的NoSQL数据库.两者在分布式.微服务架构中使用率极高,本文将用实例介绍如何在Springboot中整合Mon ...
- springboot中,使用redisTemplate操作redis
知识点: springboot中整合redis springboot中redisTemplate的使用 redis存数据时,key出现乱码问题 一:springboot中整合redis (1)pom. ...
- springboot中MongoDB的使用
转载参考:http://www.ityouknow.com/springboot/2017/05/08/spring-boot-mongodb.html MongoDB 是一个高性能,开源,无模式的文 ...
- SpringBoot中MongoDB注解概念及使用
spring-data-mongodb主要有以下注解 @Id 主键,不可重复,自带索引,可以在定义的列名上标注,需要自己生成并维护不重复的约束.如果自己不设置@Id主键,mongo会自动生成一个唯一主 ...
- Java中内部类的骚操作
10.1 如何定义内部类 如代码10.1-1 所示 public class Parcel1 { public class Contents{ private int value = 0; pu ...
- java学习(三) java 中 mongodb的各种操作
一. 常用查询: 1. 查询一条数据:(多用于保存时判断db中是否已有当前数据,这里 is 精确匹配,模糊匹配 使用 regex...) public PageUrl getByUrl(String ...
- python list 中 remove 的骚操作/易错点
在过去的某一天(2019.3.19),有个学弟问了一个关于python list中的一个问题: 比如我们已知一个列表 [3,4,5,6,5,4,3] 我们想删除第一个为3的元素. 我们尝试了如下几种方 ...
- 记录php中一种骚操作
$options = array( 'config' => array( 'aaa' => 111, 'bbb' => 222, ), 'headers' => array( ...
- SpringBoot中使用spring-data-jpa 数据库操作(上)
Java客户端使用Spring-Data-Jpa这个组件. Spring-Data-Jpa就是Spring对Hibernate的一个整合. 选择create在运行的时候它会自动帮我们创建一个表. sp ...
- SpringBoot中使用spring-data-jpa 数据库操作(下)
随机推荐
- Python性能测试框架:Locust实战教程
01认识Locust Locust是一个比较容易上手的分布式用户负载测试工具.它旨在对网站(或其他系统)进行负载测试,并确定系统可以处理多少个并发用户,Locust 在英文中是 蝗虫 的意思:作者的想 ...
- 谈谈你对MVVM开发模式和MVT的理解?
MVVM分为Model.View.ViewModel三者. Model 代表数据模型,数据和业务逻辑都在Model层中定义: View 代表UI视图,负责数据的展示: ViewModel 负责监听 M ...
- Spring AOP里面的通知Advice类型
@Before前置通知 在执行目标方法之前运行 @After后置通知 在目标方法运行结束之后 @AfterReturning返回通知 在目标方法正常返回值后运行 @AfterThrowing异常通知 ...
- 使用post请求登陆
1.使用post请求登陆 import requests import matplotlib.pyplot as plt url = 'https://www.ptpress.com.cn/login ...
- Jingle Bio:产品出海的最重要一课是「重营销轻技术」?
名字: Jingle Bio 开发者 / 团队: Luo Baishun 平台: Web 请简要介绍下这款产品 Jingle Bio 是一款不需要任何编程基础就可以轻松驾驭的个人网站制作工具,你可以使 ...
- Linux 手工释放Linux Cache Memory
手工释放Linux Cache Memory 为了加速操作和减少磁盘I/O,内核通常会尽可能多地缓存内存,这部分内存就是Cache Memory(缓存内存).根据设计,包含缓存数据的页面可以按需重新用 ...
- 开源新纪元:Llama 3.1超大杯405B跑分惊艳,首次超越GPT-4o,下载链接曝光!
开源巨擘Llama 3.1崭露头角,性能卓越引发热议 在科技界的瞩目下,Llama 3.1系列模型以其卓越的性能脱颖而出,尤其是其405B超大杯版本,在微软Azure-ML GitHub平台的多项评测 ...
- 七天.NET 8操作SQLite入门到实战 - (3)第七天Blazor学生管理页面编写和接口对接
前言 本章节的主要内容是完善Blazor学生管理页面的编写和接口对接. 七天.NET 8 操作 SQLite 入门到实战详细教程 第一天 SQLite 简介 第二天 在 Windows 上配置 SQL ...
- 人脸识别项目打包成exe的过程遇到的问题
我最近重新拾起了计算机视觉,借助Python的opencv还有face_recognition库写了个简单的图像识别demo,额外定制了一些内容,原本想打包成exe然后发给朋友,不过在这当中遇到了许多 ...
- c++代码实现 RSA的简易demo【偏向实践】
写在前面 [如果你还没搞明白算法具体步骤建议先去看视频了解,本demo旨在简单实践该算法] 本实践在理论上是成立的,但由于计算x的时候很容易溢出,所以观者可以理解该简易demo后对数据进行处理[以字符 ...