Spring Cloud支持MongoDB

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

稍显不足的是,MongoDB对于中文索引支持的力度不够,对于某些要求强事务的领域应用(比如金融、财务等)略显不足,至少还没有看到有成熟应用的案例。如果有,请告诉我。另外一个就是,在搭建MongoDB集群的时候,相对来说是比较复杂的。水平扩展虽然很简单,但是做Sharding的时候也需要仔细的考量。

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

Java Config

第一种方式就是采用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 Cloud Config服务(或者Nacos服务)中做如下配置:

1
2
3
4
5
6
7
8
9
bimgis:
mongodb:
connection: mongodb://192.168.201.155:27011,192.168.201.128:27011,192.168.201.128:27012/?replicaSet=share
auth:
user: admin
password: Admin@123
#授权的数据库
source: admin
database: bimgis_project

直接采用注解的方式

1
2
3
4
5

spring:
data:
mongodb:
uri: mongodb://admin:Admin%40123@192.168.201.155:27011,192.168.201.128:27011,192.168.201.128:27012/bimgis_sync?replicaSet=share&authSource=admin

其他的参数,请参考链接[2], 如图:

定义DAO对象

在选择定义DAO对象的时候,我使用的是Kotin语言。

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;
}
...
}

使用MongoTemplate查询DB并支持分页

MongoTemplete的使用相当的直观[3],这里不做示范,直接参考官方文档即可。需要说明的时候,MongoDB的分页支持,需要自己实现。这里贴出MongoDB分页查询的工具类,以供参考[4]

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);
}

}

使用Spring Data Repository接口

Spring Data中的Repository是一个比较核心的抽象概念,它标识将数据访问层的代码尽量的声明式使用,而非写一大堆的模板代码。在Spring Data背后,可以使结合具体的底层存储系统,比如SQL, MongoDB, ElaticSearch等等,都是通过引入额外的支持库。由于Spring是自动配置的,所以只要引入相应的starter, 就可以了。

先引入Spring Data MongoDB的starter依赖

1
2
libraries.springDataRest = "org.springframework.boot:spring-boot-starter-data-rest:${versions.springBootVersion}"
libraries.springDataMongoDB = "org.springframework.boot:spring-boot-starter-data-mongodb:${versions.springBootVersion}"

再声明DAO对象对应的Repository

1
2
3
4
5
@RepositoryRestResource(exported = true)
interface SectionRepository : MongoRepository<Section?, String?>{
@Query("{ 'project_code' : ?0,'section_name': ?1}")
fun findByProjectCodeAndSectionName(projectCode: String?, sectionName: String?): MutableIterable<Section?>?
}

自动配置Repository

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableMongoRepositories(basePackages = ["your_own_package_name"])
class ResourceExposureConfig : RepositoryRestConfigurerAdapter() {
/**
* 之所以配置这个,是因为想在接口查询的时候返回主键
*/
override fun configureRepositoryRestConfiguration(config: RepositoryRestConfiguration) {
config.exposeIdsFor(Section::class.java
)
}
}

在业务层注入Repository,直接使用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("/")
@Api(value = "接口描述", tags = ["接口描述"])
class ProjectController {
private val logger: Logger = LoggerFactory.getLogger(this.javaClass)

@Autowired
private lateinit var sectionRepo: SectionRepository

@Autowired
private lateinit var mongoPageHelper: MongoPageHelper

@PostMapping("demo")
@ApiOperation(value = "")
fun someInterface(@RequestBody x: X): ResponseMessage<X> {
//在可以就可以开心的调用Repository的各种接口了
TODO()
}
}

如果官方提供的CrudRepository和QueryByExampleExecutor还不满足的话,直接按照规则声明对应的接口就行了。参考官方文档[5]。当然更多的支持的是查询。如果想要去修改数据,或者用复杂的规则去更新数据的话,可能mongoTemplate更适合你。

结束。后面随时补充。


  1. 1.Spring Data MongoDB https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#introduction
  2. 2.Spring Boot程序配置项:https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html
  3. 3.MongoTempate: https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mongo-template
  4. 4.MongoPageHelepr原始博客: https://www.cnblogs.com/woshimrf/p/mongodb-pagenation-performance.html
  5. 5.Spring Reposiroty 自定义查询:https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mongodb.repositories.queries