Skip to content
第 204 / 250 章中间件⏱ 14 分钟阅读

第 204 章:MongoDB 入门与建模

学习目标

  • 理解 MongoDB 文档模型
  • 掌握 CRUD 与聚合
  • 学会 Mongoose / Spring Data MongoDB
  • 了解文档建模最佳实践

一、MongoDB 简介

MongoDB 是最流行的文档型 NoSQL 数据库,数据以 BSON(二进制 JSON)存储。

特点:

  • ✅ 灵活 schema
  • ✅ 嵌套文档
  • ✅ 水平扩展(分片)
  • ✅ 强大的聚合管道

二、核心概念

MongoDB关系型
DatabaseDatabase
CollectionTable
DocumentRow
FieldColumn
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 mongoose

7.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/mydb

8.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([...])
建模原则
嵌入一对少、读多
引用一对多、经常更新

动手练习

  1. 创建 users collection,实现 CRUD
  2. 用聚合统计每个年龄段的用户数
  3. 实现 Mongoose User 模型
  4. 用 $lookup 实现订单关联用户

推荐阅读


下一章:第 205 章:中间件综合实战

本站基于 VitePress 构建 · 由 Codebook 团队维护