关于 mongodb-mongoose
mongodb-mongoose 为 Node.js 和 Next.js 应用提供 MongoDB 数据建模与 Mongoose 开发指导,面向构建和维护数据库功能的开发者。内容涵盖模式设计、查询优化、Atlas 连接和迁移,并提供验证流程。
主要功能
- 01按查询模式设计 MongoDB 文档
- 02Mongoose 模型、验证与中间件指导
- 03仓储模式和聚合管道示例
- 04Atlas 连接与迁移策略
- 05查询性能分析和验证流程
适用场景
- 01为应用设计集合与文档结构
- 02实现带验证的 Mongoose 模型
- 03编写聚合查询并检查性能
- 04规划数据库迁移并验证变更
MCP 相关
首选使用 MongoDB MCP;不可用时可通过 mongosh、Atlas UI、本地模式文件和模型检查进行回退。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ practicalswan/ agent-skills --skill mongodb-mongoose安装后在 Claude Code 中输入/mongodb-mongoose确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
CHANGELOG.mdLICENSE.txtexamples/recipe-api-example.mdreferences/aggregation-reference.mdindexing-strategies.mdscripts/seed-database.js详细介绍
Mongodb Mongoose
针对当前 MongoDB 服务器版本、Mongoose 8.x+、Node.js 22+ 和 TypeScript 5.5+ 优化。
为 Node.js/Next.js 应用提供全面的 MongoDB 数据库设计、Mongoose ODM 模式和 Atlas 集成指南。
- 在可用时,利用原生并行子 Agent 调度和 200k+ 上下文窗口。
何时使用此技能
使用“症状 -> 操作”触发条件:符合任一条件时,应用此技能并按照下述协议进行验证。
- 设计 MongoDB 架构和数据模型
- 使用验证和中间件构建 Mongoose 模型
- 实现用于数据访问的仓储模式
- 编写用于复杂查询的聚合管道
- 管理 MongoDB Atlas 连接和配置
- 将 MongoDB 集成到 Next.js API 路由
- 数据库迁移策略
跨客户端可移植性
此技能旨在兼容 GitHub Copilot、Claude Code 和 Codex。
- GitHub Copilot:将文件夹放在 Copilot 可见的技能路径中;如果无法发现文件夹,则将工作流封装在项目说明中。
- Claude Code:将文件夹放在本地技能目录或兼容的插件源中。
- Codex:将文件夹安装或同步到
$CODEX_HOME/skills/mongodb-mongoose,并在重大变更后重启 Codex。
MCP 可用性及回退方案
首选 MCP 服务:MongoDB MCP
- 回退提示词:“不使用 MCP,改用 Mongodb Mongoose 技能。遵循文档说明的本地或手动回退方案,展示所选工具界面,并报告验证证据。”
- MCP 服务不可用时,使用
mongosh、MongoDB Atlas UI、本地架构文件和 Mongoose 模型检查。 - 完成变更前,针对本地或预发布数据库验证索引、查询和聚合管道。
- 如果当前宿主环境未提供 MCP 操作,不要声称使用了该操作。
反模式
- 默认将文档建模为规范化表:MongoDB 的性能取决于由查询驱动的数据形状,而非关系模型的纯粹性。
- 每次请求都返回完整的已实例化文档:在 API 路径中,过量获取和实例化开销会迅速累积。
- 未针对写入路径测试就添加中间件:钩子可能会悄然改变创建、更新和迁移行为。
验证协议
在声称“技能已成功应用”之前:
- 通过/未通过:Mongodb Mongoose 实现需指明目标运行时、框架版本和受影响的文件。
- 通过/未通过:针对变更部分运行构建、代码检查、测试或等效的本地验证。
- 通过/未通过:处理错误、依赖项漂移和环境差异等边界情况,或明确说明这些情况不在范围内。
- 压力测试场景:将工作流应用于一个通过正常路径测试但在某个边界条件下失败的变更。
- 成功指标:不作出未经测试的成功声明;每项实现声明都须对应一条命令或一个产物。
前后示例
// Before
const recipes = await Recipe.find({ author: userId }).populate('author');
// After
const recipes = await Recipe.find({ author: userId, isPublished: true })
.select({ title: 1, slug: 1, createdAt: 1 })
.sort({ createdAt: -1 })
.lean();
缩小查询形状,避免不必要的实例化,并使结果与实际需要的视图模型保持一致。
架构设计
数据建模原则
- 当数据会被一起访问且是一对少量关系时,使用嵌入
- 当数据会被独立访问,或存在一对多/多对多关系时,使用引用
- 围绕查询模式设计架构,而不是围绕规范化的关系模型
- 有策略地使用反规范化来提升读取性能
Mongoose 模型模式
import mongoose from 'mongoose';
const recipeSchema = new mongoose.Schema({
title: {
type: String,
required: [true, 'Title is required'],
trim: true,
maxlength: [200, 'Title cannot exceed 200 characters'],
index: true,
},
slug: {
type: String,
unique: true,
lowercase: true,
},
ingredients: [{
name: { type: String, required: true },
amount: { type: Number, required: true },
unit: { type: String, enum: ['g', 'kg', 'ml', 'l', 'cup', 'tbsp', 'tsp', 'piece'] },
}],
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true,
},
tags: [{ type: String, lowercase: true, trim: true }],
isPublished: { type: Boolean, default: false },
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true },
});
// Indexes for common queries
recipeSchema.index({ title: 'text', tags: 'text' });
recipeSchema.index({ author: 1, createdAt: -1 });
// Virtual fields
recipeSchema.virtual('ingredientCount').get(function() {
return this.ingredients.length;
});
// Pre-save middleware
recipeSchema.pre('save', function(next) {
if (this.isModified('title')) {
this.slug = this.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
}
next();
});
export const Recipe = mongoose.models.Recipe || mongoose.model('Recipe', recipeSchema);
架构最佳实践
- 始终定义
required、type和验证规则 - 使用
timestamps: true自动生成createdAt/updatedAt - 为经常查询的字段添加索引
- 对取值固定的字段使用
enum - 为计算属性定义虚拟属性
- 使用中间件(pre/post 钩子)处理副作用
仓储模式
class RecipeRepository {
async findAll(filter = {}, options = {}) {
const { page = 1, limit = 20, sort = '-createdAt', populate = '' } = options;
const skip = (page - 1) * limit;
const [recipes, total] = await Promise.all([
Recipe.find(filter)
.sort(sort)
.skip(skip)
.limit(limit)
.populate(populate)
.lean(),
Recipe.countDocuments(filter),
]);
return {
data: recipes,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
};
}
async findById(id) {
return Recipe.findById(id).populate('author', 'name avatar').lean();
}
async create(data) {
const recipe = new Recipe(data);
return recipe.save();
}
async update(id, data) {
return Recipe.findByIdAndUpdate(id, data, {
new: true,
runValidators: true,
});
}
async delete(id) {
return Recipe.findByIdAndDelete(id);
}
async search(query, options = {}) {
return this.findAll(
{ $text: { $search: query } },
{ ...options, sort: { score: { $meta: 'textScore' } } }
);
}
}
export const recipeRepository = new RecipeRepository();
聚合管道
常见模式
// Group recipes by tag with counts
const tagStats = await Recipe.aggregate([
{ $match: { isPublished: true } },
{ $unwind: '$tags' },
{ $group: { _id: '$tags', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 20 },
]);
// Author statistics with lookup
const authorStats = await Recipe.aggregate([
{ $group: {
_id: '$author',
recipeCount: { $sum: 1 },
avgRating: { $avg: '$rating' },
}},
{ $lookup: {
from: 'users',
localField: '_id',
foreignField: '_id',
as: 'authorInfo',
}},
{ $unwind: '$authorInfo' },
{ $project: {
name: '$authorInfo.name',
recipeCount: 1,
avgRating: { $round: ['$avgRating', 1] },
}},
{ $sort: { recipeCount: -1 } },
]);
// Date-based analytics
const monthlyRecipes = await Recipe.aggregate([
{ $match: { createdAt: { $gte: new Date('2024-01-01') } } },
{ $group: {
_id: { $dateToString: { format: '%Y-%m', date: '$createdAt' } },
count: { $sum: 1 },
}},
{ $sort: { _id: 1 } },
]);
Atlas 连接
连接设置(Next.js)
import mongoose from 'mongoose';
const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
throw new Error('MONGODB_URI environment variable is not defined');
}
let cached = global.mongoose;
if (!cached) {
cached = global.mongoose = { conn: null, promise: null };
}
export async function connectDB() {
if (cached.conn) return cached.conn;
if (!cached.promise) {
cached.promise = mongoose.connect(MONGODB_URI, {
bufferCommands: false,
});
}
cached.conn = await cached.promise;
return cached.conn;
}
连接最佳实践
- 在开发环境中缓存连接,以防止创建多个连接
- 使用
bufferCommands: false进行显式错误处理 - 在生产环境中通过
maxPoolSize设置连接池大小 - 使用带有
retryWrites=true&w=majority的 Atlas 连接字符串
迁移策略
文档版本控制
const userSchema = new mongoose.Schema({
schemaVersion: { type: Number, default: 2 },
// ... fields
});
userSchema.pre('save', function(next) {
if (this.schemaVersion < 2) {
// Migrate old fields to new format
this.schemaVersion = 2;
}
next();
});
批量迁移脚本
async function migrateUsers() {
const batchSize = 100;
let processed = 0;
let batch;
do {
batch = await User.find({ schemaVersion: { $lt: 2 } }).limit(batchSize);
for (const user of batch) {
user.schemaVersion = 2;
await user.save();
processed++;
}
console.log(`Migrated ${processed} users`);
} while (batch.length === batchSize);
}
性能提示
- 对只读查询使用
.lean()(返回普通对象,速度快 5-10 倍) - 使用
.select()仅返回所需字段 - 创建与查询模式匹配的复合索引
- 在聚合中尽早使用
$project,以减少工作集 - 避免在高频查询中使用
$lookup;改用反规范化 - 使用
explain()分析查询性能
故障排除
| 问题 | 解决方案 |
|---|---|
| 查询缓慢 | 添加索引,使用 .lean(),通过 explain() 检查 |
| 连接超时 | 检查 Atlas 网络访问权限,增大连接池 |
| 验证错误 | 检查架构约束,检查中间件顺序 |
| 重复键错误 | 确保索引唯一,并使用 try/catch 处理 |
| 内存问题 | 对大型数据集使用游标,并限制批次大小 |
常见陷阱
- 默认按规范化的关系型架构建模数据:MongoDB 的性能取决于由查询驱动的文档结构,而不是先设计表。
- 到处返回完整的水合文档:如果使用精简投影即可满足需求,水合和过度获取会增加成本。
- 添加中间件却不对写入路径进行明确测试:钩子可能会悄然改变创建、更新和迁移流程的行为。
参考资料与资源
文档
脚本
- 数据库填充 — 无依赖的 MongoDB 数据填充脚本,包含示例食谱数据
示例
- 食谱 API 示例 — 完整的 Mongoose + Next.js 食谱 CRUD API,包含模型、路由和验证
相关技能
- javascript-development:当工作流还需要现代 JavaScript 和 TypeScript 应用代码时使用。
- nextjs-development:当工作流还需要 Next.js App Router 和服务端优先的 React 模式时使用。
- sql-development:当工作流还需要 SQL 查询、架构和性能调优工作时使用。
- code-quality:当工作流还需要两阶段审查(先检查规范符合性,再检查代码质量)、可维护性和重构指导时使用。
评论
支持 Markdown