MongoDB的体系结构
MongoDB 被称为「最像关系型数据库的非关系型数据库」,这句话的根据就在它的体系结构里。这里把 Database → Collection → Document 三层结构、BSON 数据格式、_id 主键机制一次性讲清,并落到交友项目工程的 @Document 实体与连接配置上。
纲要
- 三层逻辑结构:Database → Collection → Document,与 MySQL 的 Database → Table → Row 逐级对照
- Document:BSON 格式的数据载体,Schema 灵活的收益与代价
- BSON:为什么不用纯 JSON,类型扩展与长度前缀
_id主键:ObjectId 的 12 字节结构,自研全局唯一;自指定_id的场景- 存储引擎一句话:WiredTiger
- 工程落点:
@Document(collection = "xxx")与spring.data.mongodb.uri - 按数据特征分流:什么放 MongoDB、什么留 MySQL
- 常见误解澄清
关于「交友项目为什么引入 MongoDB」(数据量大、读多写少、地理位置查询这些原因),上一篇MongoDB:介绍已经分析过,这里不再展开,只聚焦「数据在里面长什么样、怎么组织」。
三层逻辑结构:从 MySQL 对照过来
MongoDB 的逻辑结构是一个层次结构,由三部分组成,面向用户(写应用代码时操作的就是这三层):
- Document(文档):数据的最小单元,相当于 MySQL 表里的一行记录
- Collection(集合):文档的容器,相当于 MySQL 的表
- Database(数据库):集合的容器,一个 MongoDB 实例可以承载多个数据库
用 Mermaid 把两边的层次摆在一起看:
#mermaid-svg-yOd8myRqGFXOArOJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yOd8myRqGFXOArOJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yOd8myRqGFXOArOJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yOd8myRqGFXOArOJ .error-icon{fill:#552222;}#mermaid-svg-yOd8myRqGFXOArOJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yOd8myRqGFXOArOJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yOd8myRqGFXOArOJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yOd8myRqGFXOArOJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yOd8myRqGFXOArOJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yOd8myRqGFXOArOJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yOd8myRqGFXOArOJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yOd8myRqGFXOArOJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yOd8myRqGFXOArOJ .marker.cross{stroke:#333333;}#mermaid-svg-yOd8myRqGFXOArOJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yOd8myRqGFXOArOJ p{margin:0;}#mermaid-svg-yOd8myRqGFXOArOJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yOd8myRqGFXOArOJ .cluster-label text{fill:#333;}#mermaid-svg-yOd8myRqGFXOArOJ .cluster-label span{color:#333;}#mermaid-svg-yOd8myRqGFXOArOJ .cluster-label span p{background-color:transparent;}#mermaid-svg-yOd8myRqGFXOArOJ .label text,#mermaid-svg-yOd8myRqGFXOArOJ span{fill:#333;color:#333;}#mermaid-svg-yOd8myRqGFXOArOJ .node rect,#mermaid-svg-yOd8myRqGFXOArOJ .node circle,#mermaid-svg-yOd8myRqGFXOArOJ .node ellipse,#mermaid-svg-yOd8myRqGFXOArOJ .node polygon,#mermaid-svg-yOd8myRqGFXOArOJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yOd8myRqGFXOArOJ .rough-node .label text,#mermaid-svg-yOd8myRqGFXOArOJ .node .label text,#mermaid-svg-yOd8myRqGFXOArOJ .image-shape .label,#mermaid-svg-yOd8myRqGFXOArOJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-yOd8myRqGFXOArOJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yOd8myRqGFXOArOJ .rough-node .label,#mermaid-svg-yOd8myRqGFXOArOJ .node .label,#mermaid-svg-yOd8myRqGFXOArOJ .image-shape .label,#mermaid-svg-yOd8myRqGFXOArOJ .icon-shape .label{text-align:center;}#mermaid-svg-yOd8myRqGFXOArOJ .node.clickable{cursor:pointer;}#mermaid-svg-yOd8myRqGFXOArOJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yOd8myRqGFXOArOJ .arrowheadPath{fill:#333333;}#mermaid-svg-yOd8myRqGFXOArOJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yOd8myRqGFXOArOJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yOd8myRqGFXOArOJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yOd8myRqGFXOArOJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yOd8myRqGFXOArOJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yOd8myRqGFXOArOJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yOd8myRqGFXOArOJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yOd8myRqGFXOArOJ .cluster text{fill:#333;}#mermaid-svg-yOd8myRqGFXOArOJ .cluster span{color:#333;}#mermaid-svg-yOd8myRqGFXOArOJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yOd8myRqGFXOArOJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yOd8myRqGFXOArOJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-yOd8myRqGFXOArOJ .icon-shape,#mermaid-svg-yOd8myRqGFXOArOJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yOd8myRqGFXOArOJ .icon-shape p,#mermaid-svg-yOd8myRqGFXOArOJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yOd8myRqGFXOArOJ .icon-shape .label rect,#mermaid-svg-yOd8myRqGFXOArOJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yOd8myRqGFXOArOJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yOd8myRqGFXOArOJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yOd8myRqGFXOArOJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MongoDB
database
collection 集合
document 文档
field 域
MySQL
database
table 表
row 行
column 字段
逐级对照表(这张表值得记住,后面看代码、写查询时全部用得上):
| MySQL | MongoDB | 说明 |
|---|---|---|
| database | database | 数据库,含义一致 |
| table | collection | 表 / 集合 |
| row | document | 一行数据 / 一条文档 |
| column | field | 字段 / 域 |
| primary key | _id |
MongoDB 自动把 _id 字段设为主键 |
| index | index | 两边都支持索引 |
| table joins(JOIN) | 嵌套文档 / 引用 | MongoDB 不支持跨集合 JOIN |
结构落到工程里,交友项目把「MySQL 管核心数据、MongoDB 管社交行为数据」分成了两个独立的 dubbo 模块,目录如下:
tree
mf
├── mf-app-server # 应用入口,聚合各服务
├── mf-autoconfig
├── mf-commons
├── mf-dubbo
│ ├── mf-dubbo-db # MySQL 侧服务提供者(MyBatis-Plus)
│ ├── mf-dubbo-interface # 公共 API 接口
│ └── mf-dubbo-mongo # MongoDB 侧服务提供者(SpringData-Mongo)
└── mf-model
└── com.mf.model.mongo # MongoDB 实体集中在这里
两个差异要点单独说:
Collection 不强制 schema。 MySQL 建表时列就定死了,每行都必须长一个样;集合只规定「这里面放文档」,不规定文档必须有哪些字段。同一个集合里的两个文档可以有不同的字段、不同的类型。这是 MongoDB 与关系型数据库最大的差异。
灵活是有代价的。 没有数据库层的一致性约束,意味着「字段一致性」要靠应用层自己保证,写入的代码、实体类约定、团队规范就是你的 schema。省掉 ALTER TABLE 的代价是:谁都可以悄悄塞进一个新字段,读的那方却未必知道。工程里的做法是用统一的实体类(如 RecommendUser)约束读写结构,让 schema 定义收敛在代码里而不是散落在文档中。
Document:BSON 格式的数据载体
文档是一条数据的载体,内部使用 BSON(Binary JSON,二进制化的 JSON)存储,写起来和 JSON 一样遵循 key-value 结构。
拿讲义里的例子对照:MySQL 中两条记录,含 uuid、name、age 三个字段;用文档描述就是:
javascript
// 一条文档 ≈ MySQL 的一行记录
{ "uuid": 1, "name": "mf", "age": 40 }
{ "uuid": 2, "name": "", "age": 30 }
// 多条文档组成一个集合 ≈ MySQL 的一张表
{ "uuid": 1, "name": "mf", "age": 40 }
{ "uuid": 2, "name": "", "age": 30 }
对照着看:uuid/name/age 相当于 MySQL 的字段(列) ,一条文档相当于一行 ,集合相当于一张表。这就是「最像关系型数据库」的直观含义。
为什么不用纯 JSON,要搞一个二进制版本?
- 类型扩展 。JSON 只有 string、number、boolean、null、array、object 几种类型,日期得存成字符串,二进制存不了。BSON 补上了
Date(自新纪元起的毫秒数,不存时区)、ObjectId、Binary、Decimal128、内嵌文档、正则等类型。这对 Java 端很实际,Date字段反序列化回来就是java.util.Date,而不是一段要自己解析的字符串。 - 遍历更快 。BSON 在每个文档头部放了一个长度前缀,扫描时可以直接跳到下一条文档的位置,不用逐字符解析到结尾;纯 JSON 要靠括号配对才知道一条记录在哪结束。
- 字段顺序。一般场景下字段顺序没有意义,比较和查询不依赖它;但 BSON 不允许同一个文档里出现重复的 key,这一点和 JSON 的宽松写法不同。
顺带一提单文档上限:BSON 文档最大 16MB。看起来不小,但如果把图片、视频这类大对象直接塞进文档是会撞墙的,大文件该走对象存储,MongoDB 里只存元数据(项目的小视频方案就是这么拆的,文件存储方案另有一篇专门讲)。
_id 主键与 ObjectId
和 MySQL 一样,MongoDB 支持主键保证唯一性,主键字段固定叫 _id 。不指定时,驱动会自动生成一个 ObjectId。
ObjectId 是 12 字节的标识,结构拆开是这样:
text
ObjectId: 63e8a1f2 | 8f3b2c | 4d1e | 00a7b1
──────── ────── ──── ──────
字段: 时间戳 机器标识 进程ID 计数器
长度: 4字节 5字节 3字节 3字节
作用: 生成秒级 主机指纹 区分 同一秒内
的时间信息 +随机数 多进程 递增
关键结论:自研的全局唯一算法,不需要数据库自增。时间戳保证时间上递增,机器标识 + 进程 ID 区分不同节点的不同进程,末尾 3 字节计数器保证同一进程同一秒内不重复。分布式环境下各节点独立生成,天然不冲突,这也是它和 MySQL 自增主键最本质的区别:自增主键的分配权在数据库单点上,ObjectId 的分配权在每个客户端手里。
_id 也可以自己指定。业务上有天然唯一键时,直接拿来当 _id 很常见。对照前面 MySQL 章节里 tb_user_info 用 userId 做主键关联的设计,用户资料表不用单独的自增 id,而是跟着用户主键走,MongoDB 里同样的思路完全可行:把业务 id 写进 _id,一举两得,既省一次索引又天然防重。
项目工程的 RecommendUser 用的是自动生成的 ObjectId,但保留了 userId、toUserId 两个业务 id 字段去关联 MySQL 里的用户:
java
package com.mf.model.mongo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Document(collection = "recommend_user")
public class RecommendUser implements java.io.Serializable {
private ObjectId id; //主键id,自动生成的 ObjectId
private Long userId; //推荐的用户id
private Long toUserId; //用户id
private Double score =0d; //推荐得分
private String date; //日期
}
MongoDB 里只存用户 id,头像、昵称这些资料仍通过 userId 调接口/查 MySQL 拿,MongoDB 与 MySQL 各存一半,靠业务主键对齐。这个「文档内只存 id 引用」的写法,就是 MongoDB 版的「外键关联」:不做 JOIN,应用层自己拼。
存储引擎一句话
MongoDB 3.2 起默认存储引擎是 WiredTiger:文档级并发控制(写不同文档互不阻塞)、数据压缩、检查点(checkpoint)机制保证崩溃恢复。知道这三个词能对上号即可,属运维向内容,需要时再深入。
落到工程:@Document 与连接配置
概念映射到代码只有两句话:
- 一个
@Document类对应一个集合 。@Document(collection = "recommend_user")里的recommend_user就是集合名,类里字段对应文档里的域。这直接对应下一章 SpringData-Mongo 的用法:MongoTemplate操作这个类,等价于操作这个集合。 - 连接串指定数据库 。
spring.data.mongodb.uri末尾的mf就是 Database 层的名字。
mf-dubbo-mongo 模块的 application.yml 实际配置(完整实读):
yaml
server:
port: 18082
spring:
application:
name: mf-dubbo-mongo
cloud:
nacos:
discovery:
server-addr: 192.168.136.160:8848
data:
mongodb:
uri: mongodb://192.168.136.160:27017/mf #27017 是 MongoDB 默认端口,mf 是库名
dubbo:
protocol:
name: dubbo
port: 20882
registry:
address: spring-cloud://localhost
scan:
base-packages: com.mf.dubbo.api #dubbo中包扫描
三层结构与配置的对应关系:
| 体系结构层 | 工程里的落点 |
|---|---|
| Database | uri 中的 mf |
| Collection | @Document(collection = "recommend_user") |
| Document | RecommendUser 类的一个实例 |
_id |
private ObjectId id |
生产环境 MongoDB 一般还要配用户名密码(连接串形如 mongodb://user:pass@host:27017/mf),课程环境为省事没有开认证,自己的环境别照抄。
按数据特征分流
选型的完整论证在文件存储方案介绍MongoDB:介绍里已经给出,这里只聚焦一条可操作的判断线,按数据特征分流:
| 数据特征 | 归宿 | 交友项目中的例子 |
|---|---|---|
| 结构灵活多变、读写频繁、无强事务要求 | MongoDB | 动态(圈子)、推荐结果、小视频元数据、点赞、评论、访客记录、位置信息 |
| 强事务、多表关联查询 | MySQL | 订单、账户、用户核心资料 |
| 格式简单、查询极频繁、可丢可重建 | Redis | 用户缓存 |
一条数据拿不准放哪时,先问两个问题:需不需要事务回滚保证?需不需要跨实体 JOIN? 两个都答「不」,MongoDB 就合适;答一个「是」,回 MySQL。项目把推荐结果(recommend_user)放 MongoDB 而账号放 MySQL,正是这套判断的直接结果。
常见误解澄清
| 误解 | 实际情况 |
|---|---|
| 「MongoDB 没有 schema」 | 是无强制 schema,不是无 schema。集合不校验字段,但应用层仍要靠实体类和团队规范约定结构,否则字段会越写越乱 |
| 「一个集合只能存一种结构」 | 技术上可以混存不同字段的文档;但不该这么用。混存会让查询和维护成本失控,一个集合对应一类实体才是常规做法 |
「_id 只能自动生成」 |
可以自己指定任何唯一值。有天然业务键(如用户 id)时自指定反而更优 |
| 「BSON 就是 JSON」 | BSON 是 JSON 的二进制扩展:多了 Date、ObjectId、Binary、Decimal128 等类型,且有长度前缀使遍历更快;它排除了重复 key |
| 「集合可以无限大,文档想多大就多大」 | 单个 BSON 文档硬上限 16MB。大对象应外置到对象存储,文档里只存元数据 |
官方文档
总结
- 三层结构逐级对上:Database → Collection → Document 对应 MySQL 的库 → 表 → 行;field 对应 column,
_id对应 primary key,MongoDB 不支持 JOIN - Collection 不强制 schema,灵活的代价是字段一致性靠应用层保证,用统一的实体类把 schema 收敛在代码里
- BSON 不是「另一个 JSON」:扩展了类型系统(
Date、ObjectId、Binary等),长度前缀让遍历更快,单文档 16MB 上限 - ObjectId 自研全局唯一(时间戳 4 + 机器标识 5 + 进程 3 + 计数器 3),不需要自增机制;业务天然键可以直接占用
_id - 工程落点:
@Document(collection = "recommend_user")一个类对应一个集合,uri 里的mf对应库,这套映射就是下一章 SpringData-Mongo 操作的基础
结构清楚了,数据长什么样也知道了,下一篇MongoDB 的入门:命令行操作就是真正上手:建库、建集合、增删改查。
命令行操作
前面已经把 MongoDB 的体系结构(文档/集合/数据库)和数据类型讲清楚了,这一节直接上手:用 shell 把建库、增删改查、索引完整走一遍。这些命令不是为了背,而是为后面用 SpringData-Mongo 写 Java 代码打底,驱动里每个 API 都能在命令行里找到原型。概念部分不再重复,只补一句指引。
纲要
- 进入 shell :
mongosh与旧版mongo的区别、带地址连接 - 库与集合 :
show dbs/use/ 隐式创建 /dropDatabase/createCollection/drop - 文档 CRUD
- 插入:
insertOne/insertMany,自动生成ObjectId - 查询:
find/findOne,比较与逻辑操作符,$gt/$lt/$in/$or/$and与 SQL WHERE 对照 - 更新:
updateOne/updateMany,$set指定字段,$inc自增(点赞计数) - 删除:
deleteOne/deleteMany - 嵌套文档与数组 :点号路径查询、
$push、$size - 索引 :
getIndexes/createIndex/ 唯一索引防重 /explain初窥 - SQL ↔ MongoDB 速查表
- 完整验证:建库 → 插 → 查 → 改 → 删,每步给预期输出
这里用到的命令一图流:
tree
MongoDB shell 操作
├── 库
│ ├── show dbs # 列出所有库
│ ├── use mf # 切换/隐式创建库
│ ├── db # 查看当前库
│ └── db.dropDatabase() # 删除当前库
├── 集合
│ ├── show collections # 列出集合
│ ├── db.createCollection() # 显式建集合
│ └── db.user.drop() # 删除集合
├── 文档
│ ├── insertOne / insertMany # 插入
│ ├── find / findOne # 查询
│ ├── updateOne / updateMany # 更新(配 $set / $inc)
│ └── deleteOne / deleteMany # 删除
└── 索引与调优
├── getIndexes() # 查看索引
├── createIndex() # 建索引(可加 unique)
└── explain() # 看执行计划
进入 shell:mongosh 还是 mongo
课程环境里 MongoDB 跑在虚拟机的 Docker 容器中,对外暴露 27017 端口(安装方式见上一节,不重复)。连接前先分清两个客户端:
| 命令 | 版本 | 说明 |
|---|---|---|
mongo |
MongoDB 5.x 及更早自带 | 旧版 legacy shell,基于 ACE 引擎,6.0 起已移除 |
mongosh |
MongoDB 6.0+ 自带 | 新版 Node.js 版 shell,语法兼容旧版,输出格式化更好 |
先看服务器上装的是哪个,容器里可以 mongosh 也可以 mongo,本机客户端建议装 mongosh。本机连远程虚拟机:
bash
# 新版客户端,直接跟连接串
mongosh mongodb://192.168.136.160:27017
# 旧版客户端写法一致
mongo mongodb://192.168.136.160:27017
连上后提示符会显示当前库,默认落在 test(新版 mongosh 直接是 test>)。之前演示项目时也用过 Navicat 这类图形客户端建库删库,图形工具点右键就能做的事,命令行对应的就下面这几条,这节的价值在于把这些操作翻译成后面 Java 代码里的方法调用。
库操作:隐式创建是新手第一个坑
javascript
// 查看所有数据库
show dbs
// 切换数据库
use mf
// 查看当前所在库
db
// 删除当前库(先 use 过去再删)
db.dropDatabase()
坑在这里:use mf 执行完,立刻 show dbs 会发现 mf 根本不在列表里。
javascript
test> use mf
switched to db mf
mf> show dbs
admin 132.00 KiB
config 48.00 KiB
local 72.00 KiB
这就是 MongoDB 的隐式创建 :use 只是把「当前库」的指针切过去,此时库里没有任何数据,show dbs 不显示。直到往里插入第一条数据,库才真正落盘。这和 MySQL 的 CREATE DATABASE 显式建库完全是两套思路,从 SQL 过来的人几乎都会在这里愣一下:我明明 use 成功了,库呢?
#mermaid-svg-JDRAq7YaQX1B0LRz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JDRAq7YaQX1B0LRz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JDRAq7YaQX1B0LRz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JDRAq7YaQX1B0LRz .error-icon{fill:#552222;}#mermaid-svg-JDRAq7YaQX1B0LRz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JDRAq7YaQX1B0LRz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JDRAq7YaQX1B0LRz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JDRAq7YaQX1B0LRz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JDRAq7YaQX1B0LRz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JDRAq7YaQX1B0LRz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JDRAq7YaQX1B0LRz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JDRAq7YaQX1B0LRz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JDRAq7YaQX1B0LRz .marker.cross{stroke:#333333;}#mermaid-svg-JDRAq7YaQX1B0LRz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JDRAq7YaQX1B0LRz p{margin:0;}#mermaid-svg-JDRAq7YaQX1B0LRz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JDRAq7YaQX1B0LRz .cluster-label text{fill:#333;}#mermaid-svg-JDRAq7YaQX1B0LRz .cluster-label span{color:#333;}#mermaid-svg-JDRAq7YaQX1B0LRz .cluster-label span p{background-color:transparent;}#mermaid-svg-JDRAq7YaQX1B0LRz .label text,#mermaid-svg-JDRAq7YaQX1B0LRz span{fill:#333;color:#333;}#mermaid-svg-JDRAq7YaQX1B0LRz .node rect,#mermaid-svg-JDRAq7YaQX1B0LRz .node circle,#mermaid-svg-JDRAq7YaQX1B0LRz .node ellipse,#mermaid-svg-JDRAq7YaQX1B0LRz .node polygon,#mermaid-svg-JDRAq7YaQX1B0LRz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JDRAq7YaQX1B0LRz .rough-node .label text,#mermaid-svg-JDRAq7YaQX1B0LRz .node .label text,#mermaid-svg-JDRAq7YaQX1B0LRz .image-shape .label,#mermaid-svg-JDRAq7YaQX1B0LRz .icon-shape .label{text-anchor:middle;}#mermaid-svg-JDRAq7YaQX1B0LRz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JDRAq7YaQX1B0LRz .rough-node .label,#mermaid-svg-JDRAq7YaQX1B0LRz .node .label,#mermaid-svg-JDRAq7YaQX1B0LRz .image-shape .label,#mermaid-svg-JDRAq7YaQX1B0LRz .icon-shape .label{text-align:center;}#mermaid-svg-JDRAq7YaQX1B0LRz .node.clickable{cursor:pointer;}#mermaid-svg-JDRAq7YaQX1B0LRz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JDRAq7YaQX1B0LRz .arrowheadPath{fill:#333333;}#mermaid-svg-JDRAq7YaQX1B0LRz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JDRAq7YaQX1B0LRz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JDRAq7YaQX1B0LRz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JDRAq7YaQX1B0LRz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JDRAq7YaQX1B0LRz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JDRAq7YaQX1B0LRz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JDRAq7YaQX1B0LRz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JDRAq7YaQX1B0LRz .cluster text{fill:#333;}#mermaid-svg-JDRAq7YaQX1B0LRz .cluster span{color:#333;}#mermaid-svg-JDRAq7YaQX1B0LRz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JDRAq7YaQX1B0LRz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JDRAq7YaQX1B0LRz rect.text{fill:none;stroke-width:0;}#mermaid-svg-JDRAq7YaQX1B0LRz .icon-shape,#mermaid-svg-JDRAq7YaQX1B0LRz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JDRAq7YaQX1B0LRz .icon-shape p,#mermaid-svg-JDRAq7YaQX1B0LRz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JDRAq7YaQX1B0LRz .icon-shape .label rect,#mermaid-svg-JDRAq7YaQX1B0LRz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JDRAq7YaQX1B0LRz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JDRAq7YaQX1B0LRz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JDRAq7YaQX1B0LRz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 只是切换上下文
库尚未创建
写入第一条文档
use mf
show dbs 看不到 mf
db.user.insertOne(...)
库 + user 集合同时落盘
show dbs 可见 mf
删除数据库没有「确认弹窗」,db.dropDatabase() 直接执行,所以在生产上这条命令要格外小心,建议养成先 db 确认自己当前在哪个库的习惯。
集合操作:同样是隐式创建
集合(collection)对应 SQL 里的表,创建逻辑与库一致,插入文档时集合自动创建。
javascript
// 查看当前库的集合(show tables 也可以,效果相同)
show collections
// 显式创建集合(多数场景用不到,但无伤大雅)
db.createCollection("user")
// 删除集合,成功返回 true
db.user.drop()
实际开发里很少写 createCollection,insertOne 一执行,集合就有了。显式建集合的唯一常见理由是创建时顺手带参数(比如固定集合 capped collection),项目项目里用不到,知道有这条命令即可。
插入文档:insertOne 与 insertMany
MongoDB 存的是 BSON(Binary JSON),所以插入就是往里塞 JSON 风格的文档。旧版教程里常见的 insert() 是老写法,新版 shell 推荐 insertOne / insertMany,语义更明确:
javascript
// 插入单条
db.user.insertOne({id: 1, username: 'zhangsan', age: 20})
// 批量插入
db.user.insertMany([
{id: 2, username: 'lisi', age: 21},
{id: 3, username: 'wangwu', age: 22},
{id: 4, username: 'zhaoliu', age: 22}
])
insertOne 的返回值里能看到 MongoDB 自动生成的主键:
javascript
{
acknowledged: true,
insertedId: ObjectId('6516a3f2c1e4a5b6d7e8f9a0')
}
没写 _id 时,驱动自动生成一个 12 字节的 ObjectId(时间戳 + 机器标识 + 进程号 + 自增序列),全局唯一且自带时间信息。也可以自己指定 _id,只要不重复。体系结构一节提过的「MongoDB 自动将 _id 设为主键」,落地就是这行返回值。
查询:find 与条件操作符
find(query, fields) 两个参数都可选:第一个是查询条件,第二个是投影(控制返回哪些字段)。
javascript
db.user.find() // 查全部
db.user.findOne({id: 1}) // 只返回第一条匹配
db.user.find({id: 1}) // 等值条件
db.user.find({}, {id: 1, username: 1}) // 投影:只要 id 和 username
db.user.find().count() // 条数
db.user.find().sort({id: -1}) // 按 id 倒序(-1 降序,1 升序)
db.user.find().limit(2).skip(1) // 跳过 1 条取 2 条,分页的雏形
条件不等于值时,用 $ 开头的操作符。这是 MongoDB 和 SQL 思维差异最大的地方,SQL 把条件写在 WHERE 后面,MongoDB 把条件变成文档结构的一部分:
| SQL 写法 | MongoDB 写法 | 说明 |
|---|---|---|
WHERE age = 20 |
{age: 20} |
等值直接写 |
WHERE age < 20 |
{age: {$lt: 20}} |
lt = less than |
WHERE age <= 20 |
{age: {$lte: 20}} |
lte = less than or equal |
WHERE age > 20 |
{age: {$gt: 20}} |
gt = greater than |
WHERE age >= 20 |
{age: {$gte: 20}} |
gte = greater than or equal |
WHERE age != 20 |
{age: {$ne: 20}} |
ne = not equal |
WHERE id IN (1, 2) |
{id: {$in: [1, 2]}} |
in 取数组 |
WHERE a = 1 AND b = 2 |
{a: 1, b: 2} |
逗号并列即 AND |
WHERE a = 1 OR b = 2 |
{$or: [{a: 1}, {b: 2}]} |
$or 接条件数组 |
WHERE NOT a = 1 |
{a: {$ne: 1}} |
多数场景 $ne 够用 |
这张表建议贴在手边,后面写 Java 代码时,Criteria 对象的每个方法都能对回这里的操作符。跑几个例子:
javascript
// 年龄小于等于 21
db.user.find({age: {$lte: 21}})
// id 为 1 或 2
db.user.find({$or: [{id: 1}, {id: 2}]})
// 等值 + 范围组合:id 为 1 且年龄大于 20
db.user.find({id: 1, age: {$gt: 20}})
关于输出格式:老教程里的 db.user.find().pretty() 是旧版 mongo shell 的美化输出命令;新版 mongosh 默认就是格式化的 ,pretty() 依然兼容但已经没必要写。
javascript
testdb> db.user.find()
[
{
_id: ObjectId('6516a3f2c1e4a5b6d7e8f9a0'),
id: 1,
username: 'zhangsan',
age: 20
},
{
_id: ObjectId('6516a3f2c1e4a5b6d7e8f9a1'),
id: 2,
username: 'lisi',
age: 21
}
]
更新:不加 $set 是最经典的灾难坑
更新语法的原型是 update(query, update, upsert, multi),新版拆成了 updateOne / updateMany。第一个参数 query 相当于 SQL 的 WHERE,第二个参数是要更新的内容,相当于 SET 子句,但必须用 $set 包住。
javascript
// 正确:只改 age 字段
db.user.updateOne({id: 1}, {$set: {age: 22}})
// 错误示范:整文档替换!
db.user.updateOne({id: 1}, {age: 25})
第二个错误写法执行不会报错,但效果是把 id=1 的文档整个替换成 {_id: ..., age: 25},username、自定义的 id 字段全部消失。数据不是「改错了」,是「没了」。这是 MongoDB 使用事故里排名最靠前的一种,写的时候宁可多打五个字符,也要带上 $set。
#mermaid-svg-hbGwVrtbMOi7aUgz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hbGwVrtbMOi7aUgz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hbGwVrtbMOi7aUgz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hbGwVrtbMOi7aUgz .error-icon{fill:#552222;}#mermaid-svg-hbGwVrtbMOi7aUgz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hbGwVrtbMOi7aUgz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hbGwVrtbMOi7aUgz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hbGwVrtbMOi7aUgz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hbGwVrtbMOi7aUgz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hbGwVrtbMOi7aUgz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hbGwVrtbMOi7aUgz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hbGwVrtbMOi7aUgz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hbGwVrtbMOi7aUgz .marker.cross{stroke:#333333;}#mermaid-svg-hbGwVrtbMOi7aUgz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hbGwVrtbMOi7aUgz p{margin:0;}#mermaid-svg-hbGwVrtbMOi7aUgz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hbGwVrtbMOi7aUgz .cluster-label text{fill:#333;}#mermaid-svg-hbGwVrtbMOi7aUgz .cluster-label span{color:#333;}#mermaid-svg-hbGwVrtbMOi7aUgz .cluster-label span p{background-color:transparent;}#mermaid-svg-hbGwVrtbMOi7aUgz .label text,#mermaid-svg-hbGwVrtbMOi7aUgz span{fill:#333;color:#333;}#mermaid-svg-hbGwVrtbMOi7aUgz .node rect,#mermaid-svg-hbGwVrtbMOi7aUgz .node circle,#mermaid-svg-hbGwVrtbMOi7aUgz .node ellipse,#mermaid-svg-hbGwVrtbMOi7aUgz .node polygon,#mermaid-svg-hbGwVrtbMOi7aUgz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hbGwVrtbMOi7aUgz .rough-node .label text,#mermaid-svg-hbGwVrtbMOi7aUgz .node .label text,#mermaid-svg-hbGwVrtbMOi7aUgz .image-shape .label,#mermaid-svg-hbGwVrtbMOi7aUgz .icon-shape .label{text-anchor:middle;}#mermaid-svg-hbGwVrtbMOi7aUgz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hbGwVrtbMOi7aUgz .rough-node .label,#mermaid-svg-hbGwVrtbMOi7aUgz .node .label,#mermaid-svg-hbGwVrtbMOi7aUgz .image-shape .label,#mermaid-svg-hbGwVrtbMOi7aUgz .icon-shape .label{text-align:center;}#mermaid-svg-hbGwVrtbMOi7aUgz .node.clickable{cursor:pointer;}#mermaid-svg-hbGwVrtbMOi7aUgz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hbGwVrtbMOi7aUgz .arrowheadPath{fill:#333333;}#mermaid-svg-hbGwVrtbMOi7aUgz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hbGwVrtbMOi7aUgz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hbGwVrtbMOi7aUgz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hbGwVrtbMOi7aUgz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hbGwVrtbMOi7aUgz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hbGwVrtbMOi7aUgz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hbGwVrtbMOi7aUgz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hbGwVrtbMOi7aUgz .cluster text{fill:#333;}#mermaid-svg-hbGwVrtbMOi7aUgz .cluster span{color:#333;}#mermaid-svg-hbGwVrtbMOi7aUgz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hbGwVrtbMOi7aUgz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hbGwVrtbMOi7aUgz rect.text{fill:none;stroke-width:0;}#mermaid-svg-hbGwVrtbMOi7aUgz .icon-shape,#mermaid-svg-hbGwVrtbMOi7aUgz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hbGwVrtbMOi7aUgz .icon-shape p,#mermaid-svg-hbGwVrtbMOi7aUgz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hbGwVrtbMOi7aUgz .icon-shape .label rect,#mermaid-svg-hbGwVrtbMOi7aUgz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hbGwVrtbMOi7aUgz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hbGwVrtbMOi7aUgz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hbGwVrtbMOi7aUgz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} {$set: {age: 22}}
{age: 25}
updateOne({id:1}, X)
X 的写法
只改 age 字段
其余字段保留
整文档替换
仅剩 _id 和 age
其他字段全丢
安全
数据事故
几个补充行为,都实测过:
javascript
// $set 一个不存在的字段 → 直接新增该字段(文档模型无固定列的好处)
db.user.updateOne({id: 2}, {$set: {sex: 1}})
// 匹配不到文档 → 默认什么都不发生,不报错也不插入
db.user.updateOne({id: 99}, {$set: {sex: 1}})
// 第三个参数传 true(upsert)→ 匹配不到就按条件 + 更新内容插入新文档
db.user.updateOne({id: 3}, {$set: {sex: 1}}, true)
旧版 update() 默认只更新匹配的第一条,想全改得传 multi: true;新版直接用 updateOne(改一条)和 updateMany(改全部)拆开,方法名本身就说明了影响范围,不易踩。
$inc 自增:数值字段原地加减,不用先查再写。项目项目里动态的点赞数就是这么累加的,用户点一次赞,计数器加一,天然无并发覆盖问题(操作在数据库端原子完成):
javascript
// 点赞数 +1
db.movement.updateOne({_id: movementId}, {$inc: {likeCount: 1}})
// 取消赞则传负数
db.movement.updateOne({_id: movementId}, {$inc: {likeCount: -1}})
删除:deleteOne 与 deleteMany
javascript
// 删除第一条匹配
db.user.deleteOne({id: 2})
// 删除所有匹配
db.user.deleteMany({age: 22})
// 清空集合(条件为空对象)
db.user.deleteMany({})
旧版写法 db.user.remove({age: 22}, true) 的第二个参数 justOne 表示只删一条,对应现在的 deleteOne。另外注意,想清空整个集合时,大集合用 db.user.drop() 比 deleteMany({}) 快得多,后者逐条删除并记录日志,前者直接移除集合再重建元数据。
嵌套文档与数组:点号路径
文档模型的优势在嵌套结构上体现得最直接,一句带过:关系库要拆表 join 的数据,MongoDB 一个文档存完。查询时用点号路径直接下钻:
javascript
// 文档里嵌了 settings 子文档
db.user.insertOne({
id: 10,
username: 'sunqi',
settings: {strangerQuestion: '你喜欢什么运动', likeNotification: true}
})
// 用 "settings.strangerQuestion" 查嵌套字段(注意加引号)
db.user.find({'settings.strangerQuestion': '你喜欢什么运动'})
// 数组字段
db.user.updateOne({id: 10}, {$push: {tags: '运动'}}) // 往数组尾插元素
db.user.find({tags: {$size: 1}}) // 按数组长度查
点号路径里的子字段名如果含特殊字符或大写,必须整体加引号,这是 shell 命令写错的高发点。
索引:MongoDB 也要建,防重同样靠唯一索引
数据量一大,没有索引的查询就是全集合扫描,几十万条文档能查到秒级以上。命令就两个:
javascript
// 查看集合现有索引
db.user.getIndexes()
// 为 age 建升序索引(1 升序,-1 降序)
db.user.createIndex({age: 1})
// 唯一索引:username 重复的插入会被拒绝
db.user.createIndex({username: 1}, {unique: true})
getIndexes() 的返回里总有一个 _id_ 索引,主键索引自动创建,删不掉:
javascript
[
{ v: 2, key: { _id: 1 }, name: '_id_' },
{ v: 2, key: { age: 1 }, name: 'age_1' }
]
本项目系列前面吃过的「缺唯一索引导致重复数据」的亏,在 MongoDB 这里是同一个坑。比如黑名单表(一个用户对同一目标只能拉黑一次),复合唯一索引直接兜底:
javascript
// userId + blackUserId 联合唯一,重复拉黑的插入会报 E11000
db.black_list.createIndex({userId: 1, blackUserId: 1}, {unique: true})
建了索引之后,用 explain() 验证查询有没有真的走索引:
javascript
db.user.find({age: {$gt: 20}}).explain()
看返回里 winningPlan(最佳执行计划)的 stage 字段:
| stage | 含义 |
|---|---|
COLLSCAN |
全集合扫描,没走索引,数据量大时要警惕 |
IXSCAN |
索引扫描,走了索引 |
FETCH |
按索引取回完整文档 |
IDHACK |
直接按 _id 取,最快 |
没有 age 索引时上面查询的 stage 链是 COLLSCAN;createIndex({age: 1}) 之后再 explain,就变成 IXSCAN + FETCH。性能排查的第一步就是看这个字段。
SQL ↔ MongoDB 速查表
从 SQL 迁移过来,把两边语法钉在一起对照:
| SQL | MongoDB shell |
|---|---|
CREATE DATABASE mf; |
隐式:use mf + 插入数据 |
CREATE TABLE user (...); |
隐式:首次插入自动建集合(或 db.createCollection('user')) |
INSERT INTO user(id,username,age) VALUES(1,'zhangsan',20); |
db.user.insertOne({id: 1, username: 'zhangsan', age: 20}) |
INSERT INTO user(...) VALUES(...),(...); |
db.user.insertMany([{...}, {...}]) |
SELECT * FROM user; |
db.user.find() |
SELECT * FROM user WHERE id = 1; |
db.user.find({id: 1}) |
SELECT * FROM user WHERE age > 20 AND age < 30; |
db.user.find({age: {$gt: 20, $lt: 30}}) |
SELECT * FROM user WHERE id IN (1,2); |
db.user.find({id: {$in: [1, 2]}}) |
SELECT * FROM user WHERE id = 1 OR age = 22; |
db.user.find({$or: [{id: 1}, {age: 22}]}) |
SELECT id, username FROM user; |
db.user.find({}, {id: 1, username: 1}) |
SELECT COUNT(*) FROM user; |
db.user.countDocuments() |
SELECT * FROM user ORDER BY id DESC; |
db.user.find().sort({id: -1}) |
SELECT * FROM user LIMIT 2 OFFSET 1; |
db.user.find().skip(1).limit(2) |
UPDATE user SET age = 22 WHERE id = 1; |
db.user.updateOne({id: 1}, {$set: {age: 22}}) |
UPDATE user SET age = 22 WHERE age = 20;(全部行) |
db.user.updateMany({age: 20}, {$set: {age: 22}}) |
UPDATE user SET age = age + 1 WHERE id = 1; |
db.user.updateOne({id: 1}, {$inc: {age: 1}}) |
DELETE FROM user WHERE id = 2; |
db.user.deleteOne({id: 2}) |
DELETE FROM user; |
db.user.deleteMany({})(大集合改用 db.user.drop()) |
CREATE INDEX idx_age ON user(age); |
db.user.createIndex({age: 1}) |
CREATE UNIQUE INDEX idx_name ON user(username); |
db.user.createIndex({username: 1}, {unique: true}) |
DROP TABLE user; |
db.user.drop() |
DROP DATABASE testdb; |
use testdb 后 db.dropDatabase() |
完整验证:一条链路走到底
把整条链串一遍,每步都对照预期输出,本机就能复现:
javascript
// 第 1 步:建库(隐式)
use mf
// 预期输出:
switched to db mf
// 第 2 步:插入两条
db.user.insertMany([
{id: 1, username: 'zhangsan', age: 20},
{id: 2, username: 'lisi', age: 21}
])
// 预期输出(acknowledged 为 true 表示写入成功):
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6516a3f2c1e4a5b6d7e8f9a0'),
'1': ObjectId('6516a3f2c1e4a5b6d7e8f9a1')
}
}
// 第 3 步:确认库和集合已落盘
show dbs
// mf 40.00 KiB ← 此时才出现在列表里
show collections
// user
// 第 4 步:条件查询
db.user.find({age: {$gte: 21}})
// 预期输出:只有 lisi 那条(age=21)
[
{
_id: ObjectId('6516a3f2c1e4a5b6d7e8f9a1'),
id: 2,
username: 'lisi',
age: 21
}
]
// 第 5 步:更新($set 改字段 + $inc 自增)
db.user.updateOne({id: 1}, {$set: {age: 22}})
// 预期输出:
{ acknowledged: true, matchedCount: 1, modifiedCount: 1 }
db.user.updateOne({id: 1}, {$inc: {age: 1}})
// 再 find({id:1}),age 变成 23
// 第 6 步:删除
db.user.deleteOne({id: 2})
// 预期输出:
{ acknowledged: true, deletedCount: 1 }
// 第 7 步:收尾清场
db.user.drop() // true
db.dropDatabase() // 删除整个 mf 库
matchedCount 和 modifiedCount 是排障利器:改完没生效先看这两个数,matchedCount: 0 说明条件没匹配上(多半是字段名拼错或类型不符,id: "1" 和 id: 1 是两个东西);matchedCount: 1, modifiedCount: 0 说明文档存在但值本来就是这个,不是 bug。
常见误解澄清
| 误解 | 实际情况 |
|---|---|
use mf 执行完库就建好了 |
库还没创建,插入第一条数据后才落盘,show dbs 才可见 |
updateOne 第二个参数不加 $set 就是改那个字段 |
是整文档替换,未提及的字段全部丢失,必须 $set |
update 默认更新所有匹配的文档 |
旧版 update 默认只改第一条;新版用 updateMany 才改全部 |
| 查不到就更新失败会报错 | 静默跳过,matchedCount: 0;要「没有就插入」得用 upsert(第三参 true) |
id: "1" 和 id: 1 一样 |
字符串和数字是不同类型,条件匹配不上,插入和查询类型要一致 |
| MongoDB 无固定表结构,所以不用建索引 | 照样要建;大数据量无索引即全表扫描,防重靠唯一索引 |
deleteMany({}) 和 drop() 效果一样 |
drop() 连索引、元数据一起删且速度快得多;deleteMany 保留集合定义与索引 |
pretty() 是查询的一部分 |
只是旧版 shell 的输出美化;新版 mongosh 默认格式化,可不写 |
官方文档
总结
mongosh(6.0+)替代旧版mongo,连接串直接跟地址端口;shell 里写的是 JS,命令即函数调用- 库和集合都是隐式创建,
use完不插数据等于没建;db确认当前库、dropDatabase无确认直接删 - 更新必带
$set,否则整文档替换;$inc原子自增,对应点赞计数这类场景 - 条件查询的核心是把 SQL 的 WHERE 翻译成
$gt/$lt/$in/$or/$and操作符文档,速查表可当字典用 - 索引不是关系库的专利:
createIndex建索引,{unique: true}防重复数据,explain()的 stage 字段判断是否走了索引
命令行操作到这里收尾。下一篇进入同章的SpringData-Mongo:环境搭建,把今天这些 shell 命令翻译成 SpringData-Mongo 的 Java API,MongoTemplate 的每个方法都能在这里找到对应的命令原型。