第 204 章:MongoDB 入门与建模
学习目标
- 理解 MongoDB 文档模型
- 掌握 CRUD 与聚合
- 学会 Mongoose / Spring Data MongoDB
- 了解文档建模最佳实践
一、MongoDB 简介
MongoDB 是最流行的文档型 NoSQL 数据库,数据以 BSON(二进制 JSON)存储。
特点:
- ✅ 灵活 schema
- ✅ 嵌套文档
- ✅ 水平扩展(分片)
- ✅ 强大的聚合管道
二、核心概念
| MongoDB | 关系型 |
|---|---|
| Database | Database |
| Collection | Table |
| Document | Row |
| Field | Column |
| Embedded | 关联 |
| Reference | 外键 |
三、安装
yaml
# docker-compose.yml
services:
mongo:
image: mongo:7
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: admin
volumes:
- mongodata:/data/db
volumes:
mongodata:bash
# 连接
mongosh "mongodb://admin:admin@localhost:27017"四、CRUD
4.1 Insert
javascript
// 单条
db.users.insertOne({
name: "Tom",
email: "tom@x.com",
age: 18,
tags: ["dev", "music"],
address: { city: "Beijing", zip: "100000" },
createdAt: new Date()
});
// 批量
db.users.insertMany([
{ name: "Jerry", age: 20 },
{ name: "Bob", age: 25 }
]);4.2 Find
javascript
// 查全部
db.users.find();
// 条件
db.users.find({ age: { $gte: 18 } });
// 多条件
db.users.find({
age: { $gte: 18, $lte: 30 },
tags: "dev" // 数组包含
});
// 嵌套
db.users.find({ "address.city": "Beijing" });
// 投影(只返回某些字段)
db.users.find({}, { name: 1, email: 1, _id: 0 });
// 分页 + 排序
db.users.find().sort({ age: -1 }).skip(0).limit(10);
// 计数
db.users.countDocuments({ age: { $gte: 18 } });
// 存在
db.users.findOne({ email: "tom@x.com" });4.3 操作符
| 操作符 | 含义 |
|---|---|
$eq / $ne | 等 / 不等 |
$gt / $gte / $lt / $lte | 大小 |
$in / $nin | 包含 |
$and / $or / $not | 逻辑 |
$exists | 字段存在 |
$regex | 正则 |
$all | 全部包含 |
$elemMatch | 数组元素匹配 |
$size | 数组长度 |
javascript
// 数组查询
db.users.find({ tags: { $all: ["dev", "music"] } });
db.users.find({ tags: { $size: 2 } });
// 正则
db.users.find({ name: { $regex: /^T/, $options: "i" } });4.4 Update
javascript
// $set
db.users.updateOne(
{ name: "Tom" },
{ $set: { age: 19 } }
);
// $inc
db.users.updateOne(
{ name: "Tom" },
{ $inc: { age: 1 } }
);
// $push(数组追加)
db.users.updateOne(
{ name: "Tom" },
{ $push: { tags: "nodejs" } }
);
// upsert
db.users.updateOne(
{ name: "Alice" },
{ $set: { age: 22 } },
{ upsert: true }
);
// 批量
db.users.updateMany(
{ age: { $lt: 18 } },
{ $set: { status: "minor" } }
);4.5 Delete
javascript
db.users.deleteOne({ name: "Tom" });
db.users.deleteMany({ status: "inactive" });五、聚合管道
javascript
db.orders.aggregate([
{ $match: { status: "paid" } },
{ $group: {
_id: "$userId",
total: { $sum: "$amount" },
count: { $sum: 1 }
}},
{ $sort: { total: -1 } },
{ $limit: 10 },
{ $project: {
_id: 0,
userId: "$_id",
total: 1,
count: 1
}}
]);常用 stage:
$match:筛选$group:分组$project:字段投影$sort/$limit/$skip$lookup:关联(类似 JOIN)$unwind:数组展开
$lookup(关联查询)
javascript
db.orders.aggregate([
{ $lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "user"
}}
]);六、索引
6.1 创建
javascript
// 单字段
db.users.createIndex({ email: 1 }, { unique: true });
// 复合
db.users.createIndex({ name: 1, age: -1 });
// 多键(数组)
db.users.createIndex({ tags: 1 });
// TTL(过期自动删除)
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 });
// 文本
db.articles.createIndex({ title: "text", content: "text" });6.2 查看 / 删除
javascript
db.users.getIndexes();
db.users.dropIndex("email_1");6.3 explain
javascript
db.users.find({ email: "tom@x.com" }).explain("executionStats");七、Mongoose(Node.js)
7.1 安装
bash
pnpm add mongoose7.2 Schema
javascript
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true, lowercase: true },
age: { type: Number, min: 0, max: 150 },
tags: [String],
address: {
city: String,
zip: String
},
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('User', userSchema);7.3 CRUD
javascript
const User = require('./user');
// 创建
const user = await User.create({ name: 'Tom', email: 'tom@x.com' });
// 查
const tom = await User.findOne({ email: 'tom@x.com' });
const users = await User.find({ age: { $gte: 18 } }).sort('-createdAt').limit(10);
// 改
await User.updateOne({ email: 'tom@x.com' }, { $inc: { age: 1 } });
// 删
await User.deleteOne({ _id: id });八、Spring Data MongoDB
8.1 引入
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>8.2 配置
yaml
spring:
data:
mongodb:
uri: mongodb://admin:admin@localhost:27017/mydb8.3 实体
java
@Document(collection = "users")
public class User {
@Id
private String id;
@Indexed(unique = true)
private String email;
@Field("user_name")
private String name;
private Integer age;
private List<String> tags;
private Address address;
@CreatedDate
private LocalDateTime createdAt;
// getter / setter
}8.4 Repository
java
public interface UserRepository extends MongoRepository<User, String> {
List<User> findByName(String name);
List<User> findByAgeBetween(int min, int max);
@Query("{ 'tags': ?0 }")
List<User> findByTag(String tag);
}8.5 自定义查询
java
@Autowired
private MongoTemplate mongoTemplate;
public List<User> search(UserSearchRequest req) {
Query query = new Query();
if (req.getName() != null) {
query.addCriteria(Criteria.where("name").regex(req.getName()));
}
if (req.getMinAge() != null) {
query.addCriteria(Criteria.where("age").gte(req.getMinAge()));
}
query.with(Sort.by(Sort.Direction.DESC, "createdAt"));
query.skip((req.getPage() - 1) * req.getSize());
query.limit(req.getSize());
return mongoTemplate.find(query, User.class);
}九、文档建模原则
9.1 嵌入 vs 引用
javascript
// 嵌入:一个用户有少量地址(适合)
{
_id: 1,
name: "Tom",
addresses: [
{ city: "Beijing", zip: "100000" },
{ city: "Shanghai", zip: "200000" }
]
}
// 引用:博客评论(评论很多,适合引用)
{
_id: 1,
title: "...",
authorId: ObjectId("...")
}
// 评论单独 collection
{
postId: ObjectId("1"),
content: "..."
}经验法则:
- 一对少 → 嵌入
- 一对多 / 多对多 → 引用
- 子文档会被频繁查询 → 嵌入
9.2 反范式
为了查询性能,允许数据冗余:
javascript
// 用户
{
_id: 1,
name: "Tom"
}
// 订单(冗余用户信息)
{
_id: 100,
userId: 1,
userName: "Tom", // 冗余
amount: 200
}十、复制集与分片
10.1 复制集
- 自动故障转移
- 读扩展(secondary 可读)
10.2 分片
分片键选择很重要,影响性能。
十一、本章小结
| 操作 | MQL |
|---|---|
| 查 | find({...}) |
| 改 | updateOne({...}, {$set: {...}}) |
| 删 | deleteOne({...}) |
| 聚合 | aggregate([...]) |
| 建模 | 原则 |
|---|---|
| 嵌入 | 一对少、读多 |
| 引用 | 一对多、经常更新 |
动手练习
- 创建 users collection,实现 CRUD
- 用聚合统计每个年龄段的用户数
- 实现 Mongoose User 模型
- 用 $lookup 实现订单关联用户
推荐阅读
下一章:第 205 章:中间件综合实战 →