# 数据模型操作

URL: https://caijiao.org/express/chapter6/3-model-crud
Source: docs/express/chapter6/3-model-crud.md
Description: 本章节介绍如何在 Express 应用中使用 Mongoose 进行更复杂的数据模型操作，包括数据验证、查询优化、事务管理等。

在 Express 应用中，Mongoose 提供了丰富的 API 来操作数据库，支持从基本的增、查、改、删到更复杂的操作，如数据验证、查询优化、事务管理等。本章节将深入探讨这些高级操作，以便更好地管理和操作 MongoDB 数据。

## 数据验证

Mongoose 支持内置和自定义的字段验证，确保数据在保存之前符合预期的规则。

### 1. 内置验证

Mongoose 为常见的数据验证提供了内置功能，如 `required`、`minlength`、`maxlength`、`enum` 等。以下是一些常见的字段验证：

```js
const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true, // 必填
    minlength: 3, // 最小长度 3
  },
  email: {
    type: String,
    required: true,
    match: /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/, // 正则验证电子邮件格式
  },
});
````

### 2. 自定义验证

Mongoose 允许为字段添加自定义验证器。如果数据不符合规则，可以抛出一个错误。以下是一个自定义验证示例：

```js
const productSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
  },
  price: {
    type: Number,
    required: true,
    min: [0, '价格必须大于等于 0'],
    validate: {
      validator: function (v) {
        return v % 1 === 0; // 确保价格是整数
      },
      message: '价格必须是整数',
    },
  },
});
```

### 3. 验证执行时机

* **`save` 时验证**：Mongoose 在调用 `save()` 时验证文档字段。如果验证失败，它会抛出一个错误。
* **`validate` 时验证**：可以在不保存文档的情况下手动验证文档。

```js
const newUser = new User({ username: 'john_doe', email: 'john.doe@example.com' });
newUser.validate()
  .then(() => console.log('验证通过'))
  .catch((err) => console.error('验证失败', err));
```

## 查询优化

在进行大量数据查询时，Mongoose 提供了多种优化方法来提高查询性能。

### 1. 查询限制与分页

分页查询是处理大量数据时非常常用的操作。Mongoose 提供了 `limit` 和 `skip` 方法来进行分页。

```js
User.find()
  .skip(0) // 跳过前 0 条数据
  .limit(10) // 限制查询返回 10 条数据
  .then((users) => console.log(users))
  .catch((err) => console.error('查询失败', err));
```

### 2. 使用索引

为了提高查询性能，可以在经常查询的字段上添加索引。例如，添加一个索引以加速通过电子邮件查询用户：

```js
userSchema.index({ email: 1 }); // 为 email 字段创建升序索引
```

### 3. 投影查询

Mongoose 支持投影查询，可以指定只返回文档中的某些字段，而不是整个文档。这样可以减少网络传输的数据量，提升查询性能。

```js
User.find({ username: 'john_doe' }, 'email password') // 只返回 email 和 password 字段
  .then((user) => console.log(user))
  .catch((err) => console.error('查询失败', err));
```

### 4. 聚合查询

Mongoose 提供了聚合管道（`aggregate`）来执行更复杂的数据操作，例如排序、分组、计算等。

```js
User.aggregate([
  { $group: { _id: '$email', count: { $sum: 1 } } }, // 按 email 分组，并计算每组的文档数量
  { $sort: { count: -1 } } // 按 count 降序排列
])
  .then((result) => console.log(result))
  .catch((err) => console.error('聚合查询失败', err));
```

## 事务管理

Mongoose 还支持 MongoDB 的事务功能，允许我们在多个操作中保持一致性，尤其在涉及到多个文档更新时非常有用。

### 1. 启动事务

在 Mongoose 中，可以通过 `session` 来启动一个事务。以下是一个使用事务进行多文档更新的示例：

```js
const session = await mongoose.startSession();
session.startTransaction();

try {
  const user = await User.findOne({ username: 'john_doe' }).session(session);
  user.password = 'newSecurePassword';
  await user.save();

  const post = await Post.findOne({ author: user._id }).session(session);
  post.title = 'Updated Post Title';
  await post.save();

  // 提交事务
  await session.commitTransaction();
  console.log('事务成功');
} catch (err) {
  // 事务回滚
  await session.abortTransaction();
  console.error('事务失败', err);
} finally {
  session.endSession();
}
```

### 2. 事务回滚

如果事务中的任何操作失败，可以回滚事务，以确保数据保持一致。`abortTransaction()` 方法用于回滚事务。

## 其他高级操作

### 1. 多个模型操作

Mongoose 允许我们在事务中对多个模型进行操作。例如，你可以在一个事务中同时更新多个集合中的数据。

```js
const session = await mongoose.startSession();
session.startTransaction();

try {
  const user = await User.findOne({ username: 'john_doe' }).session(session);
  user.email = 'new.email@example.com';
  await user.save();

  const post = await Post.findOne({ author: user._id }).session(session);
  post.content = 'Updated post content';
  await post.save();

  await session.commitTransaction();
} catch (err) {
  await session.abortTransaction();
  console.error('事务失败', err);
} finally {
  session.endSession();
}
```

### 2. 数据生命周期钩子

Mongoose 提供了多种生命周期钩子（如 `pre`、`post` 钩子）来在数据保存、删除等操作前后执行特定操作。例如，你可以在保存数据之前进行加密操作，或在删除数据时执行日志记录。

#### 2.1 `pre` 钩子

```js
userSchema.pre('save', function(next) {
  if (this.isModified('password')) {
    // 对密码进行加密
    this.password = encryptPassword(this.password);
  }
  next();
});
```

#### 2.2 `post` 钩子

```js
userSchema.post('save', function(doc, next) {
  console.log('用户数据已保存:', doc);
  next();
});
```

## 总结

在本章节中，我们介绍了如何在 Express 应用中使用 Mongoose 进行更复杂的数据模型操作。通过数据验证、查询优化、事务管理等技术，我们可以在 Mongoose 中高效地进行数据操作，并确保数据的完整性和一致性。

* **数据验证**：包括内置验证、自定义验证、验证执行时机等。
* **查询优化**：使用分页、索引、投影查询和聚合操作来提高查询性能。
* **事务管理**：通过事务来确保多个操作的原子性和一致性。
* **高级操作**：包括多模型操作和数据生命周期钩子等。

这些操作使得在 Express 和 Mongoose 中构建复杂的数据驱动应用变得更加高效和可靠。
