Java 项目实战: 交友项目 - MongoDB 入门:体系结构和命令行操作

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 的每个方法都能在这里找到对应的命令原型。

相关推荐
小溪学编程1 小时前
将 Bean 放入 Spring 容器中的方式有哪些?
java·后端·spring
java_nnnn1 小时前
LeetCode刷题滑动窗口(水果成篮+找到字符串中所有字母的异位词)Java
java·开发语言·leetcode
用户3721574261351 小时前
Java 读取、修改和清空 PDF 文档属性(元数据)
java
SL_staff1 小时前
从‘改一行代码等三天’到‘改完即生效’:实施工程师的逻辑引擎避坑手记
java·产品·全栈
小羊没烦恼!1 小时前
异步编程:线程概述及使用
java·开发语言·mysql·算法·c#
hhwyqwqhhwy1 小时前
设备树之 provider 和 consumer
java·linux·前端
\光辉岁月/1 小时前
9.日期时间类
java·date·localdatetime·日期类
用户3721574261351 小时前
Java 操作 PPT 超链接:添加、修改和删除
java
需要8262 小时前
容器化 Java:镜像瘦身与 JVM 容器感知(cgroup)
java·开发语言·jvm