之前一直想试试MongoDB但总是没什么机会用,关系型数据库还是大多数应用场景的首选。最近有机会需要做个评测系统,其中涉及包含大量选项的不同种类的表单处理,同时规模并不大,遂考虑一番后决定用MongoDB来作为存储后端,正好尝尝鲜,也能减轻点CRUD的枯燥感。本文主要进行一些基本介绍以及展示使用示例,总结了一些使用的感受和体验。

MongoDB简介

MongoDB算是相当著名的一款开源NoSQL数据库了,它也已经有十多年的历史,截止到目前(文章发布)MongoDB也已经来到了4.4版本,以MongoDB数据库为基础成立的MongoDB公司目前除了MongoDB Server之外还提供了MongoDB Atlas(SaaS云服务数据库)、Charts(可视化图表)、Compass(数据库GUI工具)、Connectors(连接器)等各种配套服务和工具。有兴趣可以看一看MongoDB的发展历史(英),作为饭后读物十分不错,可以看到一款开源数据库的成长过程。

MongoDB说到底是一款开源的由C++编写的文档数据库,面向用户展现的结构与普通RDB也比较相似,其核心概念基本都能在RDB中找到对应的内容:

Mongo RDB 说明
database database 数据库
collection table 数据集合 - 数据表
document row 文档 - 数据记录
field column 字段 - 数据列
index index 索引

其中最核心的便是文档,它代表了MongoDB中的一个数据记录。MongoDB的文档结构如下图所示,很明显类似于JSON,可以包含数组和其他文档。

无schema的结构(可以有)配合丰富灵活的查询语句使得MongoDB相较于传统RDB具有很大的灵活性。

安装好MongoDB然后装个GUI(可以选择MongoDB Compass),然后打开GUI工具连接到数据库操作一番插入几条数据基本就能够明白其主要的能力。安装方法这里不赘述,因为随手一查就有,不过安装时需要注意的是MongoDB原生支持分布式架构,包括分片、主备等等,注意安装设置。

SpringData MongoDB

了解了MongoDB之后就可以将其应用到实际使用场景中,而作为独立的存储工具,又必然涉及到连接交互,因此MongoDB提供了各种语言的Driver,其中也包含了Java。随着应用复杂度增加,各种工具提供了更高阶的抽象或封装来简化对DB的操作以提高效率,SpringData便是其中之一,它作为Spring对于数据存储层访问的统一基础工具,也提供了针对MongoDB的实现。

BSON

BSON是MongoDB数据存储和网络传输的主要格式,它意为Binary JSON,它的字面意思直接表示了它被发明出来的主要原因。BSON的二进制结构包含了类型和长度信息以获取更块的解析速度和更高效的查询性能,此外BSON扩展支持了非JSON原生的数据类型如日期、二进制数据等。得益于此,所有能够在JSON中表达的数据都能够直接在MongoDB中存储、读取。

此外MongoDB的查询同样采用类JSON的格式来描述,个人觉得有一种和谐、自描述的感觉。配合各种操作符,表达能力很强且有很不错的可阅读性。这个特性也在Driver API中有所体现,以Java角度来说,数据和查询都可以使用同一种类型(org.bson.Document)来表示,非常优雅。举一个简单的查询条件例子:

1
2
3
4
{
status: "A",
$or: [ { price: { $lt: 30 } }, { item: /^p/ } ]
}

上述描述了查询选择status为”A”且price大于30或item以字符p开头(支持正则表达式)的所有文档。

Mongo Java Driver

Driver作为与MongoDB通信交互的基础,Spring Data所进行的操作最后还是会调用Driver提供的方法来发送请求,这些额外的封装操作最为重要的几点目的是查询的构建以及返回值到对象的映射,其实这也是ORM框架所做的主要内容。想较于JDBC只提供了对SQL字符串的提交处理,Mongo的Java Driver要丰富许多,直接提供了对象的解码编码以及函数式查询构建,这也意味着只用原生Driver的API也可以很好地适配面向对象以及便捷查询。

Spring Data

Spring Data作为Spring对于数据存储层访问的统一基础工具,也提供了针对MongoDB的封装。尽管原生的Java Mongo Driver已经提供了一些关键的能力,但是Spring Data并没有使用这些内容,查询的构建以及对象转换仍然是Spring自己的一套。Spring Data MongoDB看起来与Driver原生提供的功能有些重复,但是毕竟Spring Data特性更加丰富,同时提供了统一的Data Repository访问方式。不过使用Spring Data这一套也并不是没有缺点,Spring的这层包装也会显得比较重,若不是在Spring项目中使用,采用原生Driver未尝不是好的选择。

应用示例

下面分别对原生Driver和Spring Data MongoDB的使用做一些介绍和示例。

原生Driver

原生的Java Driver 4.1版本文档传送门在此,文档很全面,包括了教程、API文档、源码、更新信息等等。这里简述一些主要操作描述一番。

入口类是com.mongodb.client.MongoClient而并不是类似xxxConnection的形式,但是本质上仍然是获取连接对象。很明显Mongo Driver的抽象程度本身就比较高,MongoClient提供了多种创建方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// direct for localhost:27017
MongoClient mongoClient1= MongoClients.create();

// specify host and port
MongoClient mongoClient2= MongoClients.create(
MongoClientSettings.builder()
.applyToClusterSettings(builder ->
builder.hosts(Arrays.asList(new ServerAddress("hostOne", 27018))))
.build());

// from connection string
MongoClient mongoClient = MongoClients.create("mongodb://admin:admin@localhost:27017/?authSource=admin&readPreference=primary&ssl=false");

// print databases' name
mongoClient.listDatabaseNames().forEach(System.out::println);

通常MongoClient作为单例存在即可,通过MongoClient可以获取到com.mongodb.client.MongoDatabase代表MongoDB中的Database,而Database对象又可以获取到具体的Collection即com.mongodb.client.MongoCollection,通过Collection对象来执行对应的增删改查:

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
// get database
MongoDatabase database = mongoClient.getDatabase("mydb");

// get collection
MongoCollection<Document> collection = database.getCollection("test");

// insert
Document doc = new Document("name", "MongoDB")
.append("type", "database");
collection.insertOne(doc);

List<Document> documents = new ArrayList<Document>();
for (int i = 0; i < 100; i++) {
documents.add(new Document("i", i));
}
collection.insertMany(documents);

// query
Document myDoc = collection.find().first();

// query with filter
Document filteredDoc = collection.find(and(gt("i", 50), lte("i", 100))).first();

// update
collection.updateOne(eq("i", 10), set("i", 110));

UpdateResult updateResult = collection.updateMany(lt("i", 100), inc("i", 100));
System.out.println(updateResult.getModifiedCount());

// delete
collection.deleteOne(eq("i", 110));

DeleteResult deleteResult = collection.deleteMany(gte("i", 100));
System.out.println(deleteResult.getDeletedCount());

可见Mongo Driver提供了很多静态方法来构建查询条件,这些方法基本都属于com.mongodb.client.model.Filters这个类下。而在面向对象方面的对象映射上,MongoClient、MongoDatabase、MongoCollection均支持添加CodecRegistry来注册对象编码/解码器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// build codecRegistry with automatic POJO codecs
CodecRegistry pojoCodecRegistry = fromRegistries(MongoClientSettings.getDefaultCodecRegistry(),
fromProviders(PojoCodecProvider.builder().automatic(true).build()));

// mongo client level
MongoClientSettings settings = MongoClientSettings.builder()
.codecRegistry(pojoCodecRegistry)
.build();
MongoClient mongoClient = MongoClients.create(settings);

// mongo database level
database = database.withCodecRegistry(pojoCodecRegistry);

// mongo collection level
collection = collection.withCodecRegistry(pojoCodecRegistry);

配置了对象的codec之后就可以利用MongoCollection的泛型参数来直接执行对应类型的查询和插入(更新和删除操作与非对象codec形式无异):

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
// example POJO
public final class Person {
private ObjectId id;
private String name;
private int age;
private Address address;

// getter & setter
// ...
}

public final class Address {
private String street;
private String city;
private String zip;

// getter & setter
// ...
}

// generic collection
MongoCollection<Person> collection = database.getCollection("people", Person.class);

// insert pojo
Person ada = new Person("Ada Byron", 20, new Address("St James Square", "London", "W1"));
collection.insertOne(ada);

// query pojo
Person somebody = collection.find(eq("address.city", "Wimborne")).first();
System.out.println(somebody.getName());

Spring Data

SpringData MongoDB文档传送门在此,内容不算特别多,对于尝鲜使用来说可以忽略掉聚合、Reactive等一些“高级”特性。作为NoSQL里最像SQL的一类数据库,SpringData提供了MongoTemplate和MongoRepository两种使用方式。

MongoTemplate

与其他SpringData项目相似(例如Redis的RedisTemplate、JDBC的JdbcTemplate)提供了MongoTemplate这个核心操作类,它基本上涵盖了所有对MongoDB的支持并且提供了丰富的操作特性。

MongoTemplate的构造函数有如下三个:

1
2
3
4
5
MongoTemplate(MongoClient mongo, String databaseName);

MongoTemplate(MongoDatabaseFactory mongoDbFactory);

MongoTemplate(MongoDatabaseFactory mongoDbFactory, MongoConverter mongoConverter);

接下来看一看MongoTemplate的基本使用:

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
// example pojo
@Document("person")
public class Person {

@MongoId
private String id;
@Field("personName")
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

// getter & setter ...
// ...
}

// insert
Person p = new Person("Bob", 33);
mongoTemplate.insert(p);

// raw query
BasicQuery query = new BasicQuery("{ age : { $lt : 50 }, accounts.balance : { $gt : 1000.00 }}");
List<Person> result = mongoTemplate.find(query, Person.class);

// criteria query
List<Person> result = template.query(Person.class)
.matching(where("age").lt(50)
.and("accounts.balance").gt(1000.00d))
.all();

Person one = template.query(Person.class)
.matching(criteria.orOperator(where("name").ne("Jack"),
where("age").lt(10)))
.firstValue();

// update
template.update(RelationGroup.class)
.apply(new Update().pull("interviewees", "Jack")
.pull("interviewers", "Jack"))
.all();

// upsert
template.update(Person.class)
.matching(query(where("ssn").is(1111)
.and("firstName").is("Joe")
.and("Fraizer").is("Update"))
.apply(update("address", addr))
.upsert();

// delete
template.remove(person, "person");
template.remove(query(where("lastname").is("lannister")), Person.class);
template.findAllAndRemove(new Query().limit(3), "person");
Repository

熟悉Spring Data JPA的朋友一定对Repository接口不会陌生,同样的Spring Data MongoDB也提供了Repository接口的支持,能够通过方法名称自动生成对应的查询实现,如果在使用时偏向传统的DAO层处理逻辑或者某些查询重复使用率很高,那么选用Repository的方式会十分便捷。使用Java Configuration模式的情况下,开启MongoDB Repository需要使用注解@EnableMongoRepositories

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableMongoRepositories
class ApplicationConfig extends AbstractMongoClientConfiguration {

@Override
protected String getDatabaseName() {
return "e-store";
}

@Override
protected String getMappingBasePackage() {
return "com.oreilly.springdata.mongodb";
}
}

Repository样例以及使用示例:

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
public interface PersonRepository extends PagingAndSortingRepository<Person, String> {

// parse query by method name
List<Person> findByLastname(String lastname);

// support pageable
Page<Person> findByFirstname(String firstname, Pageable pageable);

// nested pojo
Person findByShippingAddresses(Address address);

// find first
Person findFirstByLastname(String lastname)

// support stream
Stream<Person> findAllBy();

// delete
List <Person> deleteByLastname(String lastname);
}

@Service
public sampleService {

@Autowired
private PersonRepository repository;

public Page<Person> getPersonPage(int page, int pageSize) {
return repository.findAll(PageRequest.of(page, pageSize));
}

public List<Person> findAllPerson() {
return new ArrayList<>(repository.findAll());
}

}

使用感受

在进行应用之前也有查询过什么时候应该使用MongoDB这个问题,需要决策是否应该采用MongoDB而不是RDB这一更加传统、稳妥的选择,但是查了半天也没查到比较好的解释,反倒是看到MongoDB官方有提供企业级的咨询服务来帮助客户进行应用。个人觉得暂且抛开性能方面的抉择之外,最主要的考虑因素有如下几点:

  1. 数据是否种类繁多且关联度很高、关联关系复杂,在使用时需要大量的类似join的结合操作。这是RDB的强项,如果有这个特性那么Mongo不会是第一选择。
  2. 数据操作是否存在大量的事务性跨集合(表)操作、批量操作。同样事务也是RDB的强项,MongoDB尽管也已经开始支持事务,但是若事务操作是主要操作类型,使用MongoDB或许需要考量一番。
  3. 归为一类的数据(即一个集合或一张表)其结构是否需要比较高的灵活性(字段不固定、变化大),或者结构中包含可变长度的字段,或者包含嵌套形数据。显然灵活度是MongoDB的杀手锏,使用RDB的时候碰到此类数据比较难处理,往往就是弄成Json或者自定义结构塞到某个字段下面,举个例子比如要存储一系列选项各不相同的调查问卷RDB就会比较头疼。毫无疑问如果有很多此类数据那么MongoDB绝对值得一试。
  4. 数据的量级是否会到千万或者亿级别以上,有高度伸缩性,且需要频繁查询。这一点其实有性能方面的考量,但是本质上来说还是要归功于Mongo原生的高度可扩展性和数据结构的适配。如果有这样的需求那么普通RDB基本上无法直接满足,如果同时满足上述的一些条件,Mongo会是很好的选择。

尽管有些方面Mongo不擅长,但是不擅长并不说明Mongo做不到,俗话说”天下大势,分久必合,合久必分”,在DB上也有几分意思在里面,Mongo也在支持事务、能够使用DBRef来做引用、引入schema约束等等,也有原生适配SQL且原生提供分片、副本的新型RDB出现。

再具体到Java对Mongo的使用上面,可以看到SpringData对MongoDB的解决方案依然是其统一的领域建模范畴,简单来说就是映射为Java Bean或者说POJO,但作为Java Bean就必然有字段、有类型,这其实是与MongoDB支持schema-less这一优势特性是有一定冲突的,不过话又说回来没有schema、没有结构化、没有类型安全对于逻辑代码的编写并不是好事,所以我们可以看到包括Mongo在内的很多NoSQL现在也开始支持设置schema。综合考虑来说,个人觉得如下情况在Java后端应用MongoDB是比较舒服的:

  • 直接存储文档数据,如文章或评论的内容、HTML页面等,这些内容无需经过代码逻辑处理直接返回给前端或者用户,充分利用MongoDB的特性。
  • 能够以”半结构化”的形式存储数据,在固定结构中内嵌灵活的子数据。具体到应用层面来说,仍然保留Java Bean的映射关系,在Bean中使用包括Map、List、数组等容器类型的字段或者直接使用org.bson.Document类型,此外使用抽象基类定义字段、各类数据使用不同的子类成员也是非常适配的一种建模方式。这样既不失固定结构带来的规范与便利,同时也保留了数据的灵活性。

当然这些只是一些比较浅显的使用感受,仅供参考~MongoDB远不止于此,个人对于Mongo是比较有好感的,用起来感觉不错,希望能有机会多多使用,深入挖掘。