MongoDB 入门


六月份到了,六月的第一周来写 MongoDB。

MongoDB 是一种面向文档的非关系型数据库,面向文档的意思是,把半结构化的数据存储为文档(XML、YAML、JSON 等),MongoDB 把数据存储为 BSON(Binary JSON, is a bin­ary-en­coded seri­al­iz­a­tion of JSON-like doc­u­ments)格式。

有关面向文档,看到这么一篇知乎文章:《常见 NoSQL 数据库的应用场景是怎么样的?

在文档数据库中的“文档”和传统意义的“文档”没什么关系,它不是书、信或者文章,这里说的“文档”其实是一个数据记录,这个记录能够对包含的数据类型和内容进行“自我描述”。XML 文档、HTML 文档和 JSON 文档就属于这一类。文档数据库可以包含非常复杂的数据结构,比如嵌套对象并且不需要使用特定的数据模式,每个文档可以具有完全不同的结构。

如果做一个简单映射,那么关系型数据库的,就是 MongoDB 的集合(Collection);关系型数据库的,就是 MongoDB 的文档(Document),关系型数据库的这两行数据:

ID NAME AGE
1 张三 20
2 李四 22

就是 MongoDB 中的这两个文档:

1
2
3
4
5
6
7
8
9
10
{
"ID":1,
"NAME":"张三",
"AGE":20
}
{
"ID":2,
"NAME":"李四",
"AGE":22
}

术语和概念

SQL 术语和 MongoDB 术语之间的映射关系

SQL Terms/Concepts MongoDB Terms/Concepts
database database
table collection
row document or BSON document
column field
index index
table joins $lookup, embedded documents
primary key primary key(在 MongoDB 中,primary key 是自动生成并设置为设置为 _id 字段的
aggregation (e.g. group by) aggregation pipeline(聚合管道)

一个样例文档(Document):

(对应着 SQL 中的一条数据)

1
2
3
4
5
6
7
{
_id: ObjectId("509a8fb2f3f4948bd2f983a0"),
user_id: "abc123",
age: 55,
status: 'A',
tags: ['test', '2020']
}

语法

CRUD

实际上有很多函数可用,例如删除相关的函数有 deletedeleteOnedeleteManyremovefindOneAndRemovefindAndModify() 等,下面列出我自认为常用的,此外还可以参考官方文档(当页往下翻)。

MySQL MongoDB
查找 select * from user db.user.find()
select name, age from user db.user.find({},{name:1,age:1})
select * from user where name = ‘ZhangSan’ db.user.find({name:”ZhangSan”})
select * from user where name is not null db.user.find({name:{$ne:null}})
select * from user where name like ‘%name%’ db.user.find({name:{$regex:”name”}})
db.user.find({name:/name/})
select * from user order by age desc db.user.find().sort({age:-1})
select * from user group by age 参见聚合
增加 INSERT INTO user (name, age) VALUES (‘pz’, 23) db.user.insert({name:”pz”, age:23})
插入多条 db.user.insertMany([
{name:”boy1”, age:20},
{name:”boy2”, age:18, tags:[“funny”]}
])
更改 UPDATE user SET age=21 WHERE age=20 db.user.updateMany({age:20},{$set:{age:21}})
UPDATE user SET age=21 WHERE age=20 limit 1 db.user.update({age:20},{$set:{age:21}})
删除 delete from user where age = 20 db.user.deleteMany({age:20})
delete from user where age = 20 limit 1 db.user.delete({age:100})

聚合(aggregate)

聚合可以简单理解成高级操作,官方的解释有点绕:聚合处理数据并返回结果(Aggregation operations process data records and return computed results)。

聚合有三种类型,分别是聚合管道(Aggregate Pipeline)、Map-Reduce(一般不翻译)、单用途聚合操作(Single Purpose Aggregation Operations)。一般提到聚合,指的是第一种:聚合管道。

下面逐个学习这三种聚合。


1. 聚合管道

MongoDB 指的 pipeline 像是 Java 中的流运算,或者是 linux 系统中的管道,意思是文档(数据)经历一系列的节点(当然也可以是一个节点),一个节点接一个地处理文档,上个节点运算完的结果,作为下个节点的输入,最终输出一堆聚合文档,比如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
db.user.aggregate(
[
// name和age不能为null
{
$match:{name:{$ne:null}, age:{$ne:null}}
},
// 只输出name、age、tags字段
{
$project:{name:1, age:1, tags:1}
},
// 按照age倒序排序
{
$sort:{age:-1}
}
]
)
aggregate 描述 样例 备注
$project 指定字段 db.user.aggregate([ { $project:{name:1, age:1} } ]) _id 默认指定,如果不想要,则需 _id:0
指定字段(起别名) db.user.aggregate([ { $project:{alias:”$name”} } ]) 别名在冒号前,字段名在冒号后
$match 匹配字段 db.user.aggregate([ { $match:{age:{$gt:10}} } ])
(年龄大于10岁)
$count 计数 db.user.aggregate([ { $count:”countNum” } ]) 冒号后代表展示字段名
$group 分组 db.user.aggregate([ { $group:{_id:”$age”,nameList:{$addToSet:”$name”}} } ])(按年龄统计,获取名字列表) _id 必填,指定要分组的字段,字段均需要带$符号,更多操作可参考 这篇博文
$sort 排序 db.user.aggregate([ { $sort:{age:1} } ]) 1:升序、-1:降序
如果在 $sort 操作之前发生 $project$unwind$group,则 $sort 不能使用任何索引。
$lookup 左连接 db.user.aggregate([ { $lookup:{ from:”organization”, localField:”organization_name”, foreignField:”name”, as:”orgInfo” } } ]) from:要关联的集合(表)、localField:本集合字段、foreignField:要关联的字段、as:新字段名
$limit 只取前几条 db.user.aggregate([ { $limit:10 } ])
$skip 跳过前N个文档 db.user.aggregate([ { $skip:5 } ])
$unwind 一个文档(数据)拆成多个文档 db.user.aggregate([ { $unwind:”$tags” } ]) 注意 $ 符号
$sample 随机选N个文档 db.user.aggregate([ { $sample:{size:10} } ])
$out 将结果保存成新表 db.user.aggregate([ { $limit:10 }, { $out:”test” } ])
(将 user 表的前 10 条数据存入 test 表中
$bucket 分组 db.user.aggregate([ { $bucket:{ groupBy:”$sort”, boundaries:[5,8,10,12], default:”other”, output:{ count:{$sum:1}, titles:{$push:”$sort”} } } } ]) 难以描述,用得不多,现用现查
$graphLookup 递归往上查 { $graphLookup:{ from:”user”, startWith:”$parentId”, connectFromField:”parentId”, connectToField:”_id”, as:”fatherList” } } 注意 startWith 后面跟的内容带 $ 符号,结果会按递归查到的顺序逆序排列

还有一些别的,用到了再回来补。


2. Map-Reduce

我目前用到的还不多,所以不想写了(hh),可以参考这篇文章《MongoDB Map-Reduce详细操作总结》,写得很容易理解概念。

以后需要用到了再回来补。


3. 单用途聚合操作

就是简化版的聚合管道,官方目前(Version 4.2)在文档中提到了三种,简单整理成下表:

MySQL MongoDB
计数 select count(*) from user db.collection.estimatedDocumentCount()
db.user.count()(废弃)
返回字段的所有值 (有点像)select name from user group by name db.user.distinct(“name”)(不理解的话点击看官方文档)

除了这三种应该还有的是方法,但是我分辨不出来是不是属于这一类,因此先不整理了。


操作符

操作符 描述 示例
$eq 等于 db.user.find({name:{$eq:”pz”}})
$ne 不等于 db.user.find({name:{$ne:null}})
$gt 大于 db.user.find({age:{$gt:10}})
$gte 大于等于 db.user.find({age:{$gte:10}})
$lt 小于 db.user.find({age:{$lt:10}})
$lte 小于等于 db.user.find({age:{$lte:10}})
$in 匹配 array 中指定的任何值 db.user.find({age:{$in:[10,20]}})
$nin 匹配 array 中指定的任何值之外 db.user.find({age:{$nin:[10,20]}})
$and 并且 db.user.find({$and:[{age:23},{name:”pz”}]})
$not db.user.find({name:{$not:/name/}})
$nor 都非 db.user.find({$nor:[{name:”pz”},{age:10}]})
$or db.user.find({$or:[{name:”pz”},{age:10}]})
$exists 具有指定字段(包括null) db.user.find({age:{$exists:true}})
$type 字段是指定类型 db.user.find({name:{$type:”string”}})
$regex 匹配正则表达式 db.user.find({name:{$regex:”^name.*$”}})
$expr 允许在查询语言中使用聚合表达式 db.user.find({$expr:{$gt:[“$age”,10]}})
$mod 指定字段是取模成功的 db.user.find({age:{$mod:[10,0]}})

剩下的用到再补,英文文档参考《Query and Projection Operators》,汉化文档(机翻,版本 v3.6)参考《查询和投影操作员》


Spring Data MongoDB

数据库连接、事务之类的操作就先不学习了,本篇主要学习 CRUD。


Spring 操作 MongoDB 的类有两种(在我的工作中),分别是 MongoTemplateReactiveMongoTemplate,前者是普通操作数据库,后者是响应式操作数据库(响应式以后应该会写博文),操作这两个类的 API 几乎是完全相同的,区别只在返回值上:

1
2
3
4
// 插入一条数据,返回这个数据
Object insert1 = mongoTemplate.insert(object);
// 插入一条数据,返回一个Mono
Mono<Object> insert2 = reactiveMongoTemplate.insert(object);

下文以 MongoTemplate 为例,学习 Spring Data MongoDB(其实是一样的,区别只在响应式上)。


普通操作

作为 Spring Data 中的一族,Spring Data Mongo 的普通操作(CRUD)跟其他 ORM 框架操作很相近,用过几次就很熟悉了,这里不详细用语言描述了,只整理表格如下(表格下有备注):

类型 方法 样例(方法返回值不写了,写了不直观) 描述
find mongoTemplate.find(query, User.class);
mongoTemplate.find(query, User.class, “user”); 指定表名,用于表和输出结果不匹配的情况
findById mongoTemplate.findById(id, User.class);
mongoTemplate.findById(id, User.class, “user”); 指定表名,同find
findOne mongoTemplate.findOne(query, User.class);
mongoTemplate.findOne(query, User.class, “user”); 指定表名,同find
findAll mongoTemplate.findAll(User.class);
mongoTemplate.findAll(User.class, “user”); 指定表名,同find
findDistinct mongoTemplate.findDistinct(“name”, User.class, String.class) 查指定字段(一共有四种,就写了两种)
mongoTemplate.findDistinct(query, “name”, User.class, String.class)
计数 count mongoTemplate.count(query, User.class);
mongoTemplate.count(query, “user”);
mongoTemplate.count(query, UserDto.class, “user”); 指定输出类、表名
插入 insert mongoTemplate.insert(user); 单条插入
mongoTemplate.insert(user, “user”);
mongoTemplate.insert(userList, “user”); 批量插入
mongoTemplate.insert(userList, User.class);
insertAll mongoTemplate.insertAll(userList);
保存 save mongoTemplate.save(user); 只能保存单条数据,有则更新,无则插入
mongoTemplate.save(user, “user”);
更新 updateFirst mongoTemplate.updateFirst(query, update, User.class); 修改第一条
mongoTemplate.updateFirst(query, update, “user”);
mongoTemplate.updateFirst(query, update, UserDto.class, “user”); 指定输出类、表名
updateMulti mongoTemplate.updateMulti(query, update, User.class); 修改所有
mongoTemplate.updateMulti(query, update, “user”);
mongoTemplate.updateMulti(query, update, UserDto.class, “user”); 指定输出类、表名
更新或插入 upsert mongoTemplate.upsert(query, update, User.class); 有则更新,无则插入
mongoTemplate.upsert(query, update, “user”);
mongoTemplate.upsert(query, update, User.class, “user”);
存在 exists mongoTemplate.exists(query, User.class);
mongoTemplate.exists(query, “user”);
mongoTemplate.exists(query, User.class, “user”); 第二个参数没有意义,可以为null,
查询时以第三个参数为依据
删除 remove mongoTemplate.remove(user);
mongoTemplate.remove(user, “user”);
mongoTemplate.remove(query, User.class);
mongoTemplate.remove(query, “user”);
mongoTemplate.remove(query, User.class, “user”);

备注:

  1. 查询时常用到 QueryCriteria 类,这两个类我懒得写了,贴一点代码吧,看一看就懂了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 写法1
    Query query = Query.query(Criteria.where("name").is("pz"));

    // 写法2
    Criteria criteria = Criteria.where("name").is("pz")
    .and("age").is(23)
    .and("height").gt(170);
    Query query = new Query(criteria);

    // 写法3
    Criteria criteria = Criteria.where("name").is("pz");
    Query query = new Query();
    query.addCriteria(criteria);
  2. Update 类是跟 Query 同等地位的类,用于 MongoDB 的更新操作,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 相当于{$set: {name: "pz"}},内部Java实现是return new Update().set(key, value);
    Update update = Update.update("name", "pz");

    Update update = new Update();
    // 相当于{$set: {name: "pz"}}
    update.set("name", "pz");
    // 相当于{$push: {tags: "IT"}},即tags字段(列表)增加一个元素"pz"
    update.push("tags", "IT");
    // 相当于{$inc: {age: 2}},即age字段增加2
    update.inc("age",2);
  3. find 还有几种方法:findAndModifyfindAndRemovefindAndReplacefindAllAndRemove,我觉得没什么用,没整理。


聚合操作

Spring Data MongoDB 涉及到聚合相关的类有四个,分别是:

  • Aggregation:代表着 MongoDB 的 aggregate(聚合),例如下面这个例子

    1
    2
    // 相当于db.user.aggregate([...])
    mongoTemplate.aggregate(aggregation, User.class);
  • TypedAggregation:是 Aggregation 的子类,在 Aggregation 的基础上增加了类型,来指定是对哪张表进行聚合操作。

    1
    2
    // 指定操作user表的aggregate
    TypedAggregation<User> typedAggregation = Aggregation.newAggregation(User.class, operations);
  • AggregationOperation:聚合的具体操作,有很多子类,对应着一个个的 MongoDB aggregation pipeline operation,举例而言

    1
    2
    // 相当于{$project: {a: 1, b: 1, thing2: $thing1}}
    Aggregation.project("a", "b").and("thing1").as("thing2");
  • AggregationResults:聚合查询结果(非响应式)

    1
    AggregationResults<User> result = mongoTemplate.aggregate(typedAggregation, User.class);

举一个例子,把 AggregationAggregationOperationAggregationResults 串起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// db.user.aggregate([
// {$match: {name: "pz"}},
// {$project: {name: 1, age: 1, tags: 1}},
// {$limit: 1}
// ])
AggregationResults<User> result = mongoTemplate.aggregate(
Aggregation.newAggregation(
Aggregation.match(Criteria.where("name").is("pz")),
Aggregation.project("name", "age", "tags"),
Aggregation.limit(1)
),
"user", // 数据库表
User.class // 输出类型
);

学习 Spring Data MongoDB 聚合,主要是学习 AggregationOperation 的所有子类,把各种聚合操作都熟悉之后,用 Aggregation.newAggregation() 组合在一起执行就可以了。

有关聚合的所有操作,基本都有两种创建方式:静态工厂方法和构造方法,官方推荐使用静态工厂方法创建,而非构造方法(尽管背后实现是完全相同的):

1
2
3
4
// 官方推荐,这样更易于阅读
ProjectionOperation projectionOperation1 = Aggregation.project();
// 不推荐
ProjectionOperation projectionOperation2 = new ProjectionOperation();

接下来一个个 AggregationOperation 说,把常用的聚合操作学习完。


1. ProjectionOperation

指定字段

1
2
3
4
5
6
7
8
9
10
11
12
13
// {
// $project:{
// name:1,
// age:1,
// idDelete:"$is_deleted",
// _id:0,
// nickName:1
// }
// }
ProjectionOperation projectionOperation = Aggregation.project("name", "age")
.and("is_deleted").as("idDelete")
.andExclude("_id")
.andInclude("nickName");
  • and() 需要配合 as() 一起用,表示别名,不能单独使用(因为返回类型不一样)
  • andInclude()and() 的区别是,前者单独使用,后者配合 as() 使用

2. MatchOperation

查询(相当于 Query)

1
2
3
4
5
6
7
8
// {
// $match:{
// name:"pz",
// age:{$gt: 20}
// }
// }
Criteria criteria = Criteria.where("name").is("pz").and("age").gt(20);
MatchOperation matchOperation = Aggregation.match(criteria);

3. LookupOperation

联表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// {
// $lookup:{
// from:"user",
// localField:"name",
// foreignField:"username",
// as:"userInfo"
// }
// }
LookupOperation lookupOperation1 = Aggregation.lookup("user", "name", "username", "userInfo");
// 还有一种方式,更直观
LookupOperation lookupOperation2 = LookupOperation.newLookup()
.from("user")
.localField("name")
.foreignField("username")
.as("userInfo");

4. SortOperation

1
2
3
4
5
// {
// $sort:{age: 1, grade: -1}
// }
Sort sort = Sort.by("age").and(Sort.by("grade").descending());
SortOperation sortOperation = Aggregation.sort(sort);

5. LimitOperation

取前 N 条

1
2
3
4
// {
// $limit:10
// }
LimitOperation limitOperation = Aggregation.limit(10);

6. CountOperation

计数

1
2
3
4
// {
// $count:"countNum"
// }
CountOperation countNum = Aggregation.count().as("countNum");

7. UnwindOperation

拆分字段,常见于联表查询之后

1
2
3
4
5
6
7
8
9
10
11
12
13
// {
// $lookup:{
// from:"user",
// localField:"name",
// foreignField:"username",
// as:"userInfo"
// }
// },
// {
// $unwind:"$userInfo"
// }
LookupOperation lookupOperation = Aggregation.lookup("user", "name", "username", "userInfo");
UnwindOperation unwindOperation = Aggregation.unwind("userInfo");

8. SkipOperation

跳过前 N 条数据

1
2
3
4
// {
// $skip: 100
// }
SkipOperation skipOperation = Aggregation.skip(100L);

9. SampleOperation

随机取样 N 条数据

1
2
3
4
// {
// $sample: 10
// }
SampleOperation sampleOperation = Aggregation.sample(10);

10. GroupOperation

合并

1
2
3
4
5
6
7
8
// {
// $group:{
// _id: "$appid",
// list: {$addToSet: "$account"}
// }
// }
GroupOperation groupOperation = Aggregation.group("appid")
.addToSet("account").as("list");

11. GraphLookupOperation

1
2
3
4
5
6
7
8
9
10
11
// {
// $graphLookup:{
// from:"user",
// startWith:"$parentId",
// connectFromField:"parentId",
// connectToField:"_id",
// as:"fatherList"
// }
// }
GraphLookupOperation operation4 = Aggregation.graphLookup("user").startWith("parentId")
.connectFrom("parentId").connectTo("_id").as("fatherList");

这篇就先写到这里了。