MongoDB和Spring集成

MongoDB无疑是当前在各个领域中NoSQL优选的文档数据库之一。原因在于部署简单,可扩展性强,原生支持javascript脚本编程。对于使用Spring全家桶的团队来说,是一个很好的选择,可以快速的搭建一个服务出来。如果是用JS或者Typescript语言的话,使用Mongoose的ORM框架结合到express框架,生产力会提高一大截。

稍显不足的是,MongoDB对于中文索引支持的力度不够,对于某些要求强事务的领域应用(比如金融、财务等)略显不足,至少还没有看到有成熟应用的案例。如果有,请告诉我。

另外一个就是,在搭建MongoDB集群的时候,相对来说是比较复杂的。水平扩展虽然很简单,但是做Sharding的时候也需要仔细的考量。

本文简单介绍一下如何在Spring Boot/Spring Cloud程序中使用MongoDB数据库。

连接数据库

方式一

在Spring应用引入Spring Data MongoDB的对应的Starter依赖,如果是maven的话,如下:

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<version>2.5.8</version>
</dependency>

这样的话,程序启动的时候会触发对应的MongoDB配置。我用的是本地的MongoDB,相应的配置如下:

1
2
3
4
spring:
data:
mongodb:
uri: mongodb://admin:Admin%40123@127.0.0.1:27011/test_db

然后直接在业务代码中,直接在代码中使用MongoTemplate类就可以访问数据库了。注意这里的uri可支持多种配置,比如数据库集群。具体请参考Spring的官方文档即可。

方式二

自定义Java Config文件,直接上代码,如下:

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

import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.MongoCredential;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;

@Configuration
public class MongoClientConfiguration extends AbstractMongoClientConfiguration {

@Value("${bimgis.mongodb.connection:mongodb://192.168.201.156:27017}")
private String mongoConnectionStr = "mongodb://192.168.201.156:27017";

@Value("${bimgis.mongodb.auth.user:admin}")
private String user = "admin";

@Value("${bimgis.mongodb.auth.password:}")
private String password = "";

@Value("${bimgis.mongodb.auth.source:admin}")
private String adminDatabase = "admin";

@Value("${bimgis.mongodb.database:}")
private String database;

@Override
public MongoClient mongoClient() {
//Refer http://mongodb.github.io/mongo-java-driver/3.12/driver/tutorials/connect-to-mongodb/
MongoCredential credential = MongoCredential.createCredential(user, adminDatabase, password.toCharArray());
MongoClientSettings settings = MongoClientSettings.builder()
.credential(credential)
.applyConnectionString(new ConnectionString(mongoConnectionStr))
.build();
return MongoClients.create(settings);
}

@Override
protected String getDatabaseName() {
return database;
}
}

然后在spring.yaml中配置MongoDB的属性:

1
2
3
4
5
6
7
8
9
bimgis:
mongodb:
connection: mongodb://localhost:27011
auth:
user: admin
password: Admin@123
#授权的数据库
source: admin
database: test_db

这里达到的效果和方式一是一样的。其实完全不这么做,但是当你想要把一些配置隐藏起来或者做一些额外的事情的时候,才需要这样。

定义DAO对象

Spring Data对MongoDB的支持还不错。基本上我们遇到的场景都可以解决掉,查询的效率和性能也基本可以满足要求。那么如何在程序中使用MongoDB中的存储的数据呢?

那么得先定义一个DAO对象,用于使用该对象来存储对应集合的数据。虽然MongoDB支持一个集合可以存储任意结构的数据,但是为了维护和理解更简单,还是尽量同一个集合存储同一个结构的数据。这个时候,定义DAO对象就显得很重要了。

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
@Document(collection = "section")
class Section {
@Id
var _id: String? = null

@Field("name")
var sectionName: String? = null

@Field("project_code")
var projectCode: String? = null

@Field("del")
@JsonProperty("isDelete")
var isDelete = 0

@Field("ct")
var createTime: Date? = null

@Field("cb")
var createBy: String? = null

@Field("ut")
var updateTime: Date? = null

@Field("ub")
var updateBy: String? = null
}

对代码做一些简单的说明,@Id来声明主键,在每个字段上都增加@Field注解,这样可以增加代码的清晰度。

值得注意的时候,@Field注解有很几个参数,比较关键的一个是targetType, 这个参数定义了应用和数据库之间在做序列化的时候DAO对象和MongoDB BSON存储的映射关系。如果不显示声明的话,将使用默认的映射关系。比如BigDecimal的Java类型并不会存储为Decimal128,而是转换成String进行存储。如果想要存储为Decimal128,最简单的手段就是在定义字段的时候做如下声明:

1
2
@Field(name = "balance", targetType = FieldType.DECIMAL128)
var balance: BigDecimal = BigDecimal(0.0)

在Spring Data Mongo的MongoConverters中声明了对象的转化实现,有兴趣可以阅读相关代码,这里贴出一部分代码:

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
abstract class MongoConverters {
private MongoConverters() {
}

static Collection<Object> getConvertersToRegister() {
List<Object> converters = new ArrayList();
converters.add(MongoConverters.BigDecimalToStringConverter.INSTANCE);
converters.add(MongoConverters.BigDecimalToDecimal128Converter.INSTANCE);
converters.add(MongoConverters.StringToBigDecimalConverter.INSTANCE);
converters.add(MongoConverters.Decimal128ToBigDecimalConverter.INSTANCE);
converters.add(MongoConverters.BigIntegerToStringConverter.INSTANCE);
converters.add(MongoConverters.StringToBigIntegerConverter.INSTANCE);
converters.add(MongoConverters.URLToStringConverter.INSTANCE);
converters.add(MongoConverters.StringToURLConverter.INSTANCE);
converters.add(MongoConverters.DocumentToStringConverter.INSTANCE);
converters.add(MongoConverters.TermToStringConverter.INSTANCE);
converters.add(MongoConverters.NamedMongoScriptToDocumentConverter.INSTANCE);
converters.add(MongoConverters.DocumentToNamedMongoScriptConverter.INSTANCE);
converters.add(MongoConverters.CurrencyToStringConverter.INSTANCE);
converters.add(MongoConverters.StringToCurrencyConverter.INSTANCE);
converters.add(MongoConverters.AtomicIntegerToIntegerConverter.INSTANCE);
converters.add(MongoConverters.AtomicLongToLongConverter.INSTANCE);
converters.add(MongoConverters.LongToAtomicLongConverter.INSTANCE);
converters.add(MongoConverters.IntegerToAtomicIntegerConverter.INSTANCE);
converters.add(MongoConverters.BinaryToByteArrayConverter.INSTANCE);
converters.add(MongoConverters.BsonTimestampToInstantConverter.INSTANCE);
converters.add(ConverterBuilder.reading(String.class, URI.class, URI::create).andWriting(URI::toString));
return converters;
}
...
}

如何分页查询

MongoTemplete的使用相当的直观,这里不做示范,直接参考官方文档即可。需要说明的时候,MongoDB的分页查询需要自己实现。

这里贴出MongoDB分页查询的工具类,以供参考。

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
package demo;

import com.schdri.bimgis.data.manage.common.entity.PageResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* MongoDB分页查询工具类.
**/
@Component
public class MongoPageHelper {

public static final int FIRST_PAGE_NUM = 0;
public static final int DEFAULT_PAGE_SIZE = 10;
public static final String ID = "_id";
private final MongoTemplate mongoTemplate;

@Autowired
public MongoPageHelper(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}


/**
* 分页查询,直接返回集合类型的结果.
*
* @see MongoPageHelper#pageQuery(Query,
* Class, Function, Integer, Integer,
* String)
*/
public <T> PageResult<T> pageQuery(Query query, Class<T> entityClass, Integer pageSize,
Integer pageNum, String collectionName) {
return pageQuery(query, entityClass, Function.identity(), pageSize, pageNum, null, collectionName);
}

/**
* 分页查询,默认集合、可对集合元素进行转换
*
* @see MongoPageHelper#pageQuery(Query,
* Class, Function, Integer, Integer,
* String)
*/
public <T, R> PageResult<R> pageQuery(Query query, Class<T> entityClass, Function<T, R> mapper,
Integer pageSize, Integer pageNum) {
return pageQuery(query, entityClass, mapper, pageSize, pageNum, null, null);
}

/**
* 分页查询,使用lastId分页,直接返回集合类型的结果.
*
* @see MongoPageHelper#pageQuery(Query,
* Class, Function, Integer, Integer,
* String)
*/
public <T> PageResult<T> pageQuery(Query query, Class<T> entityClass, Integer pageSize,
Integer pageNum, Long lastId, String collectionName) {
return pageQuery(query, entityClass, Function.identity(), pageSize, pageNum, lastId, collectionName);
}

/**
* 分页查询,不考虑条件分页,直接使用skip-limit来分页.
*
* @see MongoPageHelper#pageQuery(Query,
* Class, Function, Integer, Integer,
* String)
*/
public <T, R> PageResult<R> pageQuery(Query query, Class<T> entityClass, Function<T, R> mapper,
Integer pageSize, Integer pageNum, String collectionName) {
return pageQuery(query, entityClass, mapper, pageSize, pageNum, null, collectionName);
}

/**
* 分页查询.
*
* @param query Mongo Query对象,构造你自己的查询条件.
* @param entityClass Mongo collection定义的entity class,用来确定查询哪个集合.
* @param mapper 映射器,你从db查出来的list的元素类型是entityClass, 如果你想要转换成另一个对象,比如去掉敏感字段等,可以使用mapper来决定如何转换.
* @param pageSize 分页的大小.
* @param pageNum 当前页.
* @param lastId 条件分页参数, 区别于skip-limit,采用find(_id>lastId).limit分页.
* 如果不跳页,像朋友圈,微博这样下拉刷新的分页需求,需要传递上一页的最后一条记录的ObjectId。如果是null,则返回pageNum那一页.
* @param <T> collection定义的class类型.
* @param <R> 最终返回时,展现给页面时的一条记录的类型。
* @param collectionName 集合名称
* @return PageResult,一个封装page信息的对象.
*/
public <T, R> PageResult<R> pageQuery(Query query, Class<T> entityClass, Function<T, R> mapper,
Integer pageSize, Integer pageNum, Long lastId, String collectionName) {
//总条数
long total = StringUtils.isEmpty(collectionName) ? mongoTemplate.count(query, entityClass) : mongoTemplate.count(query, entityClass, collectionName);

if (pageNum == null || pageNum < 0) {
pageNum = FIRST_PAGE_NUM;
}

if (pageSize == null || pageSize <= 0) {
pageSize = DEFAULT_PAGE_SIZE;
}

//计算取记录的范围
final Integer pages = (int) Math.ceil(total / (double) pageSize);
if (pageNum > (pages - 1)) {
pageNum = FIRST_PAGE_NUM;
}

//确定分页方式
if (lastId != null && lastId > 0) {
if (pageNum != FIRST_PAGE_NUM) {
query.addCriteria(Criteria.where(ID).gt(lastId));
}
query.limit(pageSize);
} else {
int skip = pageSize * pageNum;
query.skip(skip).limit(pageSize);
}


//查询
final List<T> entityList = StringUtils.isEmpty(collectionName) ? mongoTemplate
.find(query
.with(Sort.by(new Order(Direction.ASC, ID))),
entityClass) :
mongoTemplate
.find(query
.with(Sort.by(new Order(Direction.ASC, ID))),
entityClass, collectionName);

//组织查询结果
final PageResult<R> pageResult = new PageResult<>();
pageResult.setTotal(total);
pageResult.setPages(pages);
pageResult.setPageSize(pageSize);
pageResult.setPageNum(pageNum);
pageResult.setData(entityList.stream().map(mapper).collect(Collectors.toList()));
return pageResult;
}


/**
* 内嵌列表分页查询:将内嵌的列表展开关联外层集合的数据形成新列表进行分页查询
*
* @param query 查询筛选参数
* @param entityClass 外层集合映射的实体类
* @param flatMapper 展开内嵌列表操作的Lambda表达式
* @param pageSize 每页记录数
* @param pageNum 第几页,从0开始
* @param collectionName 外层集合名称,为null则使用entityClass关联的默认集合
* @param <T> 外层集合实体类
* @param <R> 经flatMapper转换且分页操作后返回的实体类
* @return
*/
public <T, R> PageResult<R> pageQueryByInner(Query query, Class<T> entityClass, Function<T, Stream<R>> flatMapper,
Integer pageSize, Integer pageNum, String collectionName) {

//查询数据库返回外层集合列表
final List<T> entityList = StringUtils.isEmpty(collectionName) ? mongoTemplate
.find(query.with(Sort.by(new Order(Direction.ASC, ID))),
entityClass) :
mongoTemplate
.find(query.with(Sort.by(new Order(Direction.ASC, ID))),
entityClass, collectionName);

//转换为内嵌集合列表
final List<R> innerEntityList = entityList == null ? null : entityList.stream().flatMap(flatMapper).collect(Collectors.toList());

//生成分页参数
int total = innerEntityList == null ? 0 : innerEntityList.size();

if (pageNum == null || pageNum < 0) {
pageNum = FIRST_PAGE_NUM;
}

if (pageSize == null || pageSize <= 0) {
pageSize = DEFAULT_PAGE_SIZE;
}

final Integer pages = (int) Math.ceil(total / (double) pageSize);
if (pageNum > (pages - 1)) {
pageNum = FIRST_PAGE_NUM;
}


//分页返回
final PageResult<R> pageResult = new PageResult<>();
if (total > 0) {
int fromIndex = pageSize * pageNum;
int toIndexTmp = pageSize * (pageNum + 1);
int toIndex = toIndexTmp > total ? total : toIndexTmp;
pageResult.setData(innerEntityList.subList(fromIndex, toIndex));
} else {
pageResult.setData(new ArrayList<>());
}

pageResult.setTotal(Long.valueOf(total));
pageResult.setPages(pages);
pageResult.setPageSize(pageSize);
pageResult.setPageNum(pageNum);

return pageResult;
}

/**
* 不分页列表查询,指定集合的数据
*
* @param query 筛选条件
* @param entityClass 列表元素类
* @param collectionName mongoDB集合名称
* @param <T>
* @return
*/
public <T> PageResult<T> noPageQuery(Query query, Class<T> entityClass, String collectionName) {

List<T> entityList = null; //查询结果实体
Long total = 0L;//总条数

//查询
if (!StringUtils.isEmpty(collectionName)) {
total = mongoTemplate.count(query, entityClass, collectionName);
entityList = mongoTemplate.find(query, entityClass, collectionName);
} else {
total = mongoTemplate.count(query, entityClass);
entityList = mongoTemplate.find(query, entityClass);
}

final PageResult<T> pageResult = new PageResult<>();
pageResult.setTotal(total);
pageResult.setPages(1);
pageResult.setPageSize(total.intValue());
pageResult.setPageNum(0);
pageResult.setData(entityList);
return pageResult;
}

/**
* 不分页列表查询,指定集合的数据,支持数据转换
*
* @param query 筛选条件
* @param entityClass 列表元素类
* @param collectionName mongoDB集合名称
* @param <T>
* @return
*/
public <T, R> PageResult<R> noPageQuery(Query query, Class<T> entityClass, String collectionName, Function<T, R> mapper) {

List<T> entityList = null; //查询结果实体
Long total = 0L;//总条数

//查询
if (!StringUtils.isEmpty(collectionName)) {
total = mongoTemplate.count(query, entityClass, collectionName);
entityList = mongoTemplate.find(query, entityClass, collectionName);
} else {
total = mongoTemplate.count(query, entityClass);
entityList = mongoTemplate.find(query, entityClass);
}

final PageResult<R> pageResult = new PageResult<>();
pageResult.setTotal(total);
pageResult.setPages(1);
pageResult.setPageSize(total.intValue());
pageResult.setPageNum(0);
pageResult.setData(entityList.stream().map(mapper).collect(Collectors.toList()));
return pageResult;
}

/**
* 不分页列表查询,默认集合的数据
*
* @param query
* @param entityClass
* @param <T>
* @return
*/
public <T> PageResult<T> noPageQuery(Query query, Class<T> entityClass) {
return noPageQuery(query, entityClass, null);
}

}

总结

支持CURD查询的话,剩下的工作就是定义一个Controller文件和相应的接口,使用已经自动配置好的MongoTemplate和刚定义的DAO对象去存储数据就可以了。MongoTemplate提供了基本的save,remove,find,findById,upsert,update接口。如何实现业务层的逻辑,不再本文的范围之内,就不赘述了。

本文中,我们简单介绍了一下如何在Spring Boot程序中使用MongoDB,基本上大家参考这篇文章就可以将服务跑起来。在后面的文章中,我们会把视线更多的放在原理和一些最佳实践上。希望大家持续关注。

参考:

【1】Spring Data MongoDB https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#introduction

【2】Spring Boot程序配置项:https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html

【3】MongoTempate: https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mongo-template

【4】MongoPageHelepr原始博客: https://www.cnblogs.com/woshimrf/p/mongodb-pagenation-performance.html

【5】Spring Reposiroty 自定义查询:https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mongodb.repositories.queries

【6】mongotemplate api:https://docs.spring.io/spring-data/mongodb/docs/current/api/org/springframework/data/mongodb/core/MongoTemplate.html