乐闻世界logo
搜索文章和话题

服务端面试题手册

Mongoose 数据验证有哪些类型,如何实现自定义验证?

Mongoose 提供了强大的数据验证功能,可以在保存数据到数据库之前验证数据的完整性和正确性。验证可以在 Schema 层面定义,也可以自定义验证器。内置验证器1. 必填验证(required)const userSchema = new Schema({ name: { type: String, required: [true, 'Name is required'] }, email: { type: String, required: true }});2. 类型验证(type)const userSchema = new Schema({ age: Number, isActive: Boolean, birthDate: Date});3. 枚举验证(enum)const userSchema = new Schema({ status: { type: String, enum: ['active', 'inactive', 'pending'], enum: { values: ['active', 'inactive', 'pending'], message: '{VALUE} is not a valid status' } }});4. 范围验证(min, max)const userSchema = new Schema({ age: { type: Number, min: [0, 'Age must be at least 0'], max: [120, 'Age cannot exceed 120'] }, score: { type: Number, min: 0, max: 100 }});5. 长度验证(minlength, maxlength)const userSchema = new Schema({ username: { type: String, minlength: [3, 'Username must be at least 3 characters'], maxlength: [20, 'Username cannot exceed 20 characters'] }});6. 正则表达式验证(match)const userSchema = new Schema({ email: { type: String, match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 'Please fill a valid email address'] }, phone: { type: String, match: /^[0-9]{10}$/, message: 'Phone number must be 10 digits' }});7. 唯一验证(unique)const userSchema = new Schema({ email: { type: String, unique: true, index: true }});8. 默认值(default)const userSchema = new Schema({ status: { type: String, default: 'active' }, createdAt: { type: Date, default: Date.now }});自定义验证器单字段验证器const userSchema = new Schema({ password: { type: String, validate: { validator: function(v) { return v.length >= 8; }, message: 'Password must be at least 8 characters long' } }});异步验证器const userSchema = new Schema({ email: { type: String, validate: { validator: async function(v) { const user = await this.constructor.findOne({ email: v }); return !user || user._id.toString() === this._id.toString(); }, message: 'Email already exists' } }});多字段验证器const userSchema = new Schema({ password: String, confirmPassword: String});userSchema.path('confirmPassword').validate(function(v) { return v === this.password;}, 'Passwords do not match');验证时机验证在以下时机自动触发:save() - 保存文档时validate() - 显式调用验证时validateSync() - 同步验证时const user = new User({ name: '', age: -5 });try { await user.save();} catch (err) { console.log(err.errors.name.message); // "Name is required" console.log(err.errors.age.message); // "Age must be at least 0"}跳过验证在某些情况下,可以跳过验证:// 跳过验证保存await user.save({ validateBeforeSave: false });// 跳过验证更新await User.findByIdAndUpdate(id, { age: 25 }, { runValidators: false });验证错误处理userSchema.pre('validate', function(next) { if (this.password !== this.confirmPassword) { this.invalidate('confirmPassword', 'Passwords do not match'); } next();});// 捕获验证错误try { await user.save();} catch (err) { if (err.name === 'ValidationError') { Object.keys(err.errors).forEach(field => { console.log(`${field}: ${err.errors[field].message}`); }); }}最佳实践在 Schema 层面定义验证规则提供清晰的错误消息使用异步验证器检查唯一性在前端和后端都进行验证考虑性能影响,避免过于复杂的验证使用自定义验证器处理业务逻辑记录验证失败的情况
阅读 0·2月22日 20:12

Mongoose 查询构建器有哪些常用方法和优化技巧?

Mongoose 提供了强大的查询构建器,支持链式调用和丰富的查询操作,使 MongoDB 查询更加直观和易用。基本查询查找所有文档const users = await User.find();条件查询// 等于const users = await User.find({ age: 25 });// 不等于const users = await User.find({ age: { $ne: 25 } });// 大于const users = await User.find({ age: { $gt: 18 } });// 大于等于const users = await User.find({ age: { $gte: 18 } });// 小于const users = await User.find({ age: { $lt: 30 } });// 小于等于const users = await User.find({ age: { $lte: 30 } });// 在数组中const users = await User.find({ status: { $in: ['active', 'pending'] } });// 不在数组中const users = await User.find({ status: { $nin: ['deleted'] } });逻辑操作符// ANDconst users = await User.find({ age: { $gte: 18 }, status: 'active'});// ORconst users = await User.find({ $or: [ { status: 'active' }, { status: 'pending' } ]});// NOTconst users = await User.find({ status: { $not: { $eq: 'deleted' } }});// NORconst users = await User.find({ $nor: [ { status: 'deleted' }, { status: 'inactive' } ]});链式查询选择字段// 只选择特定字段const users = await User.find() .select('name email age');// 排除特定字段const users = await User.find() .select('-password -__v');// 使用对象语法const users = await User.find() .select({ name: 1, email: 1, age: 1 });排序// 升序const users = await User.find() .sort({ name: 1 });// 降序const users = await User.find() .sort({ age: -1 });// 多字段排序const users = await User.find() .sort({ status: 1, age: -1 });限制和跳过// 限制结果数量const users = await User.find() .limit(10);// 跳过指定数量const users = await User.find() .skip(5);// 分页const page = 2;const pageSize = 10;const users = await User.find() .skip((page - 1) * pageSize) .limit(pageSize);高级查询正则表达式// 包含const users = await User.find({ name: /john/i });// 以开头const users = await User.find({ name: /^john/i });// 以结尾const users = await User.find({ name: /doe$/i });数组查询// 数组包含特定值const users = await User.find({ tags: 'javascript' });// 数组包含多个值(AND)const users = await User.find({ tags: { $all: ['javascript', 'nodejs'] } });// 数组大小const users = await User.find({ tags: { $size: 3 } });嵌套文档查询// 嵌套字段查询const users = await User.find({ 'address.city': 'New York' });// 嵌套数组查询const users = await User.find({ 'orders.status': 'completed' });元素查询// 字段存在const users = await User.find({ email: { $exists: true } });// 字段类型const users = await User.find({ age: { $type: 'number' } });聚合查询// 分组统计const result = await User.aggregate([ { $match: { age: { $gte: 18 } } }, { $group: { _id: '$status', count: { $sum: 1 } } }]);// 查找并修改const user = await User.findOneAndUpdate( { email: 'john@example.com' }, { age: 25 }, { new: true });查询优化使用索引const userSchema = new Schema({ email: { type: String, index: true }, age: { type: Number, index: true }});// 复合索引userSchema.index({ email: 1, age: -1 });投影优化// 只查询需要的字段const users = await User.find() .select('name email');使用 lean()// 返回普通 JavaScript 对象,性能更好const users = await User.find().lean();批量操作// 批量插入const users = await User.insertMany([ { name: 'John', email: 'john@example.com' }, { name: 'Jane', email: 'jane@example.com' }]);// 批量更新const result = await User.updateMany( { status: 'pending' }, { status: 'active' });查询缓存Mongoose 支持查询缓存以提高性能:const userSchema = new Schema({ name: String, email: String}, { query: { cache: true }});// 启用缓存const users = await User.find().cache();// 设置缓存时间const users = await User.find().cache(60); // 60秒最佳实践为常用查询字段创建索引使用 select() 只查询需要的字段对于只读查询使用 lean() 提高性能合理使用分页避免查询大量数据避免在循环中执行查询使用批量操作代替多次单条操作监控查询性能,优化慢查询使用 explain() 分析查询计划
阅读 0·2月22日 20:12

Mongoose 插件如何创建和使用?

Mongoose 插件(Plugins)是一种可重用的机制,用于扩展 Mongoose Schema 的功能。插件允许你将通用功能封装起来,并在多个 Schema 中复用。基本插件创建简单插件// timestamp.jsfunction timestampPlugin(schema) { schema.add({ createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); schema.pre('save', function(next) { this.updatedAt = new Date(); next(); });}module.exports = timestampPlugin;使用插件const timestampPlugin = require('./plugins/timestamp');const userSchema = new Schema({ name: String, email: String});userSchema.plugin(timestampPlugin);const User = mongoose.model('User', userSchema);插件选项带选项的插件// softDelete.jsfunction softDeletePlugin(schema, options = {}) { const deletedAtField = options.deletedAtField || 'deletedAt'; const isDeletedField = options.isDeletedField || 'isDeleted'; schema.add({ [deletedAtField]: Date, [isDeletedField]: { type: Boolean, default: false } }); schema.pre('find', function() { this.where({ [isDeletedField]: { $ne: true } }); }); schema.pre('findOne', function() { this.where({ [isDeletedField]: { $ne: true } }); }); schema.methods.softDelete = function() { this[isDeletedField] = true; this[deletedAtField] = new Date(); return this.save(); };}module.exports = softDeletePlugin;// 使用带选项的插件userSchema.plugin(softDeletePlugin, { deletedAtField: 'deletedAt', isDeletedField: 'isDeleted'});常用插件类型1. 时间戳插件function timestampPlugin(schema) { schema.add({ createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); schema.pre('save', function(next) { this.updatedAt = new Date(); next(); });}2. 软删除插件function softDeletePlugin(schema) { schema.add({ deletedAt: Date, isDeleted: { type: Boolean, default: false } }); schema.pre('find', function() { this.where({ isDeleted: { $ne: true } }); }); schema.pre('findOne', function() { this.where({ isDeleted: { $ne: true } }); }); schema.methods.softDelete = function() { this.isDeleted = true; this.deletedAt = new Date(); return this.save(); }; schema.statics.findDeleted = function() { return this.find({ isDeleted: true }); };}3. 分页插件function paginatePlugin(schema) { schema.statics.paginate = function(query = {}, options = {}) { const { page = 1, limit = 10, sort = {}, populate = [] } = options; const skip = (page - 1) * limit; return Promise.all([ this.countDocuments(query), this.find(query) .sort(sort) .skip(skip) .limit(limit) .populate(populate) ]).then(([total, docs]) => ({ docs, total, page, pages: Math.ceil(total / limit) })); };}// 使用分页插件const result = await User.paginate( { status: 'active' }, { page: 1, limit: 10, sort: { name: 1 } });4. 自动填充插件function autoPopulatePlugin(schema, options) { const fields = options.fields || []; schema.pre('find', function() { fields.forEach(field => { this.populate(field); }); }); schema.pre('findOne', function() { fields.forEach(field => { this.populate(field); }); });}// 使用自动填充插件userSchema.plugin(autoPopulatePlugin, { fields: ['profile', 'settings']});5. 加密插件const bcrypt = require('bcrypt');function encryptionPlugin(schema, options) { const fields = options.fields || ['password']; schema.pre('save', async function(next) { for (const field of fields) { if (this.isModified(field)) { this[field] = await bcrypt.hash(this[field], 10); } } next(); }); schema.methods.comparePassword = async function(candidatePassword) { return bcrypt.compare(candidatePassword, this.password); };}// 使用加密插件userSchema.plugin(encryptionPlugin, { fields: ['password']});插件组合多个插件const timestampPlugin = require('./plugins/timestamp');const softDeletePlugin = require('./plugins/softDelete');const paginatePlugin = require('./plugins/paginate');userSchema.plugin(timestampPlugin);userSchema.plugin(softDeletePlugin);userSchema.plugin(paginatePlugin);插件依赖function advancedPlugin(schema, options) { // 依赖其他插件的功能 if (!schema.path('createdAt')) { throw new Error('timestampPlugin must be applied before advancedPlugin'); } // 使用其他插件添加的字段 schema.virtual('age').get(function() { return Date.now() - this.createdAt.getTime(); });}全局插件应用到所有 Schema// 全局应用插件mongoose.plugin(timestampPlugin);// 之后创建的所有 Schema 都会自动应用该插件const userSchema = new Schema({ name: String });const postSchema = new Schema({ title: String });// 两个 Schema 都会有 createdAt 和 updatedAt 字段条件全局插件// 只对特定模型应用插件mongoose.plugin(function(schema) { if (schema.options.enableTimestamps) { schema.add({ createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); }});// 在 Schema 选项中启用const userSchema = new Schema({ name: String }, { enableTimestamps: true });插件最佳实践1. 插件命名// 使用清晰的命名const timestampPlugin = require('./plugins/timestamp');const softDeletePlugin = require('./plugins/softDelete');2. 插件文档/** * 软删除插件 * * 功能: * - 添加 deletedAt 和 isDeleted 字段 * - 自动过滤已删除的文档 * - 提供 softDelete() 方法 * * 选项: * - deletedAtField: 删除时间字段名(默认:deletedAt) * - isDeletedField: 删除标记字段名(默认:isDeleted) */function softDeletePlugin(schema, options = {}) { // 插件实现}3. 插件测试// plugins/softDelete.test.jsconst mongoose = require('mongoose');const softDeletePlugin = require('./softDelete');describe('softDeletePlugin', () => { let User; beforeAll(async () => { await mongoose.connect('mongodb://localhost/test'); const userSchema = new Schema({ name: String }); userSchema.plugin(softDeletePlugin); User = mongoose.model('User', userSchema); }); it('should add soft delete fields', async () => { const user = await User.create({ name: 'John' }); expect(user.isDeleted).toBe(false); expect(user.deletedAt).toBeUndefined(); }); it('should filter deleted documents', async () => { const user = await User.create({ name: 'Jane' }); await user.softDelete(); const users = await User.find(); expect(users.length).toBe(0); });});发布插件NPM 包结构mongoose-plugin-softdelete/├── package.json├── README.md├── index.js└── test/ └── softDelete.test.jspackage.json{ "name": "mongoose-plugin-softdelete", "version": "1.0.0", "description": "Mongoose plugin for soft delete functionality", "main": "index.js", "keywords": ["mongoose", "plugin", "soft-delete"], "peerDependencies": { "mongoose": ">=6.0.0" }}最佳实践保持插件简单:每个插件只做一件事提供选项:允许用户自定义插件行为文档完善:提供清晰的文档和示例测试覆盖:为插件编写完整的测试版本管理:使用语义化版本控制错误处理:妥善处理错误情况性能考虑:避免插件影响性能向后兼容:保持 API 的向后兼容性
阅读 0·2月22日 20:12

Mongoose 如何处理事务和并发控制?

Mongoose 支持 MongoDB 的事务功能,允许在多个文档或集合之间执行原子性操作。事务是确保数据一致性的重要机制。事务基础启用事务支持MongoDB 4.0+ 支持副本集上的事务,MongoDB 4.2+ 支持分片集群上的事务。const mongoose = require('mongoose');// 连接到副本集mongoose.connect('mongodb://localhost:27017/mydb?replicaSet=myReplicaSet');创建会话const session = await mongoose.startSession();基本事务操作简单事务const session = await mongoose.startSession();session.startTransaction();try { // 执行操作 const user = await User.create([{ name: 'John' }], { session }); const account = await Account.create([{ userId: user[0]._id, balance: 100 }], { session }); // 提交事务 await session.commitTransaction(); console.log('Transaction committed');} catch (error) { // 回滚事务 await session.abortTransaction(); console.log('Transaction aborted:', error);} finally { // 结束会话 session.endSession();}更新操作事务const session = await mongoose.startSession();session.startTransaction();try { // 转账操作 const fromAccount = await Account.findById(fromAccountId).session(session); const toAccount = await Account.findById(toAccountId).session(session); if (fromAccount.balance < amount) { throw new Error('Insufficient balance'); } fromAccount.balance -= amount; toAccount.balance += amount; await fromAccount.save({ session }); await toAccount.save({ session }); await session.commitTransaction();} catch (error) { await session.abortTransaction(); throw error;} finally { session.endSession();}事务选项读写关注const session = await mongoose.startSession({ defaultTransactionOptions: { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' }, readPreference: 'primary' }});session.startTransaction({ readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' }});事务超时session.startTransaction({ maxTimeMS: 5000 // 5秒超时});并发控制乐观锁使用版本号实现乐观锁:const productSchema = new Schema({ name: String, stock: Number, version: { type: Number, default: 0 }});productSchema.pre('save', function(next) { this.increment(); next();});// 更新时检查版本号const product = await Product.findById(productId);product.stock -= quantity;try { await product.save();} catch (error) { if (error.name === 'VersionError') { console.log('Document was modified by another process'); }}悲观锁使用 findOneAndUpdate 实现悲观锁:const session = await mongoose.startSession();session.startTransaction();try { // 查找并锁定文档 const product = await Product.findOneAndUpdate( { _id: productId, locked: false }, { locked: true }, { session, new: true } ); if (!product) { throw new Error('Product is locked or not found'); } // 执行操作 product.stock -= quantity; await product.save({ session }); // 释放锁 product.locked = false; await product.save({ session }); await session.commitTransaction();} catch (error) { await session.abortTransaction(); throw error;} finally { session.endSession();}原子操作原子更新// 原子递增await User.findByIdAndUpdate(userId, { $inc: { balance: 100 } });// 原子条件更新await User.findOneAndUpdate( { _id: userId, balance: { $gte: 100 } }, { $inc: { balance: -100 } });// 原子数组操作await User.findByIdAndUpdate(userId, { $push: { tags: 'new-tag' }, $addToSet: { uniqueTags: 'unique-tag' }});批量原子操作// 批量更新await User.updateMany( { status: 'active' }, { $set: { lastActive: new Date() } });// 批量删除await User.deleteMany({ status: 'deleted' });事务最佳实践保持事务简短:事务时间越长,冲突概率越高避免长时间持有锁:尽快释放资源使用适当的隔离级别:根据业务需求选择处理重试逻辑:实现自动重试机制监控事务性能:跟踪事务执行时间和失败率合理设计数据模型:减少跨文档事务的需求// 自动重试事务async function runWithRetry(fn, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (error.name === 'TransientTransactionError' && i < maxRetries - 1) { console.log(`Retrying transaction (attempt ${i + 1})`); await new Promise(resolve => setTimeout(resolve, 100 * (i + 1))); } else { throw error; } } }}注意事项事务只能在副本集或分片集群上使用事务操作必须在同一个会话中执行事务会带来性能开销,谨慎使用避免在事务中执行耗时操作确保正确处理事务错误和回滚考虑使用原子操作替代简单事务
阅读 0·2月22日 20:08

Mongoose 聚合框架如何使用,有哪些常用操作?

Mongoose 聚合框架(Aggregation Framework)是 MongoDB 强大的数据处理工具,允许对文档进行复杂的数据转换和计算操作。Mongoose 提供了与 MongoDB 聚合管道完全兼容的接口。基本聚合操作使用 aggregate() 方法const results = await User.aggregate([ { $match: { age: { $gte: 18 } } }, { $group: { _id: '$city', count: { $sum: 1 } } }]);聚合管道阶段1. $match - 过滤文档// 过滤年龄大于等于 18 的用户const results = await User.aggregate([ { $match: { age: { $gte: 18 } } }]);// 多条件过滤const results = await User.aggregate([ { $match: { age: { $gte: 18 }, status: 'active' }}]);2. $group - 分组统计// 按城市分组统计用户数量const results = await User.aggregate([ { $group: { _id: '$city', count: { $sum: 1 }, avgAge: { $avg: '$age' } }}]);// 多字段分组const results = await Order.aggregate([ { $group: { _id: { city: '$city', status: '$status' }, totalAmount: { $sum: '$amount' }, count: { $sum: 1 } }}]);3. $project - 投影字段// 选择特定字段const results = await User.aggregate([ { $project: { name: 1, email: 1, fullName: { $concat: ['$firstName', ' ', '$lastName'] } }}]);// 排除字段const results = await User.aggregate([ { $project: { password: 0, __v: 0 }}]);4. $sort - 排序// 按年龄升序排序const results = await User.aggregate([ { $sort: { age: 1 } }]);// 多字段排序const results = await User.aggregate([ { $sort: { city: 1, age: -1 } }]);5. $limit 和 $skip - 分页// 分页查询const page = 2;const pageSize = 10;const results = await User.aggregate([ { $skip: (page - 1) * pageSize }, { $limit: pageSize }]);6. $lookup - 关联查询// 关联订单数据const results = await User.aggregate([ { $match: { _id: userId } }, { $lookup: { from: 'orders', localField: '_id', foreignField: 'userId', as: 'orders' }}]);// 多个关联const results = await User.aggregate([ { $lookup: { from: 'orders', localField: '_id', foreignField: 'userId', as: 'orders' }}, { $lookup: { from: 'reviews', localField: '_id', foreignField: 'userId', as: 'reviews' }}]);7. $unwind - 展开数组// 展开标签数组const results = await User.aggregate([ { $unwind: '$tags' }, { $group: { _id: '$tags', count: { $sum: 1 } }}]);// 保留空数组const results = await User.aggregate([ { $unwind: { path: '$tags', preserveNullAndEmptyArrays: true } }]);高级聚合操作数组操作// $push - 添加到数组const results = await User.aggregate([ { $group: { _id: '$city', users: { $push: '$name' } }}]);// $addToSet - 添加到集合(去重)const results = await User.aggregate([ { $group: { _id: '$city', uniqueTags: { $addToSet: '$tags' } }}]);// $first 和 $last - 获取第一个和最后一个const results = await User.aggregate([ { $sort: { createdAt: 1 } }, { $group: { _id: '$city', firstUser: { $first: '$name' }, lastUser: { $last: '$name' } }}]);条件操作// $cond - 条件表达式const results = await User.aggregate([ { $project: { name: 1, ageGroup: { $cond: { if: { $gte: ['$age', 18] }, then: 'adult', else: 'minor' } } }}]);// $ifNull - 空值处理const results = await User.aggregate([ { $project: { name: 1, displayName: { $ifNull: ['$displayName', '$name'] } }}]);日期操作// 按日期分组const results = await Order.aggregate([ { $group: { _id: { year: { $year: '$createdAt' }, month: { $month: '$createdAt' }, day: { $dayOfMonth: '$createdAt' } }, total: { $sum: '$amount' }, count: { $sum: 1 } }}]);// 日期范围查询const results = await User.aggregate([ { $match: { createdAt: { $gte: new Date('2024-01-01'), $lt: new Date('2025-01-01') } }}]);性能优化使用索引// 在 $match 阶段使用索引const results = await User.aggregate([ { $match: { email: 'john@example.com' } }, // 使用索引 { $group: { _id: '$city', count: { $sum: 1 } } }]);优化管道顺序// 优化前const results = await User.aggregate([ { $project: { name: 1, age: 1, city: 1 } }, { $match: { age: { $gte: 18 } } }]);// 优化后 - 先过滤再投影const results = await User.aggregate([ { $match: { age: { $gte: 18 } } }, { $project: { name: 1, age: 1, city: 1 } }]);使用 $facet 并行处理// 并行执行多个聚合管道const results = await User.aggregate([ { $facet: { total: [{ $count: 'count' }], adults: [ { $match: { age: { $gte: 18 } } }, { $count: 'count' } ], byCity: [ { $group: { _id: '$city', count: { $sum: 1 } } } ] }}]);实际应用场景销售统计const salesStats = await Order.aggregate([ { $match: { createdAt: { $gte: new Date('2024-01-01'), $lt: new Date('2025-01-01') } }}, { $group: { _id: { year: { $year: '$createdAt' }, month: { $month: '$createdAt' } }, totalRevenue: { $sum: '$amount' }, orderCount: { $sum: 1 }, avgOrderValue: { $avg: '$amount' } }}, { $sort: { '_id.year': 1, '_id.month': 1 } }]);用户活跃度分析const userActivity = await User.aggregate([ { $lookup: { from: 'activities', localField: '_id', foreignField: 'userId', as: 'activities' }}, { $project: { name: 1, email: 1, activityCount: { $size: '$activities' }, lastActivity: { $max: '$activities.createdAt' } }}, { $sort: { activityCount: -1 } }]);最佳实践尽早使用 $match:减少处理的数据量合理使用索引:在 $match 阶段利用索引限制结果数量:使用 $limit 避免处理过多数据避免过深嵌套:保持管道简洁使用 $facet:并行处理多个聚合监控性能:使用 explain() 分析聚合性能分批处理大数据:对于大数据集使用分批处理
阅读 0·2月22日 20:07

Mongoose 虚拟字段是什么,如何使用?

Mongoose 虚拟字段(Virtual Fields)是一种强大的功能,允许定义不存储在数据库中的计算字段。虚拟字段可以基于文档的其他字段动态计算值,或者创建文档之间的关联。基本虚拟字段定义虚拟字段const personSchema = new Schema({ firstName: String, lastName: String, birthDate: Date});// 定义 fullName 虚拟字段personSchema.virtual('fullName').get(function() { return `${this.firstName} ${this.lastName}`;});// 定义虚拟字段的 setterpersonSchema.virtual('fullName').set(function(name) { const parts = name.split(' '); this.firstName = parts[0]; this.lastName = parts[1];});const Person = mongoose.model('Person', personSchema);// 使用虚拟字段const person = new Person({ firstName: 'John', lastName: 'Doe' });console.log(person.fullName); // "John Doe"person.fullName = 'Jane Smith';console.log(person.firstName); // "Jane"console.log(person.lastName); // "Smith"计算字段const productSchema = new Schema({ price: Number, taxRate: { type: Number, default: 0.1 }});// 计算含税价格productSchema.virtual('priceWithTax').get(function() { return this.price * (1 + this.taxRate);});const Product = mongoose.model('Product', productSchema);const product = new Product({ price: 100, taxRate: 0.2 });console.log(product.priceWithTax); // 120虚拟字段关联一对多关联const authorSchema = new Schema({ name: String, email: String});const bookSchema = new Schema({ title: String, author: { type: Schema.Types.ObjectId, ref: 'Author' }});// 在 Author 模型上添加虚拟字段,获取所有书籍authorSchema.virtual('books', { ref: 'Book', localField: '_id', foreignField: 'author'});const Author = mongoose.model('Author', authorSchema);const Book = mongoose.model('Book', bookSchema);// 使用虚拟字段关联const author = await Author.findById(authorId).populate('books');console.log(author.books); // Array of books by this author多对多关联const studentSchema = new Schema({ name: String});const courseSchema = new Schema({ title: String, students: [{ type: Schema.Types.ObjectId, ref: 'Student' }]});// 在 Student 模型上添加虚拟字段,获取所有课程studentSchema.virtual('courses', { ref: 'Course', localField: '_id', foreignField: 'students'});const Student = mongoose.model('Student', studentSchema);const Course = mongoose.model('Course', courseSchema);// 使用虚拟字段关联const student = await Student.findById(studentId).populate('courses');console.log(student.courses); // Array of courses for this student虚拟字段选项基本选项userSchema.virtual('profileUrl', { ref: 'Profile', localField: '_id', foreignField: 'user', justOne: true, // 返回单个文档而不是数组 count: false // 返回计数而不是文档});条件虚拟字段userSchema.virtual('isAdult').get(function() { return this.age >= 18;});userSchema.virtual('status').get(function() { if (this.deleted) return 'deleted'; if (this.banned) return 'banned'; return 'active';});虚拟字段在 JSON 和查询中的行为JSON 序列化默认情况下,虚拟字段不会包含在 JSON 输出中。需要设置 toJSON 选项:const userSchema = new Schema({ firstName: String, lastName: String}, { toJSON: { virtuals: true }, toObject: { virtuals: true }});userSchema.virtual('fullName').get(function() { return `${this.firstName} ${this.lastName}`;});const user = new User({ firstName: 'John', lastName: 'Doe' });console.log(JSON.stringify(user)); // 包含 fullName查询虚拟字段虚拟字段不能直接用于查询,因为它们不存储在数据库中:// 不支持User.find({ fullName: 'John Doe' }); // 不会工作// 解决方案:使用实际字段User.find({ firstName: 'John', lastName: 'Doe' });虚拟字段的限制不能用于查询:虚拟字段不能在查询条件中使用不能用于排序:虚拟字段不能用于排序操作不能用于索引:虚拟字段不能创建索引性能考虑:每次访问都会重新计算JSON 输出:需要配置才能包含在 JSON 中实际应用场景1. 格式化显示const orderSchema = new Schema({ items: [{ name: String, price: Number, quantity: Number }]});orderSchema.virtual('totalPrice').get(function() { return this.items.reduce((sum, item) => { return sum + (item.price * item.quantity); }, 0);});2. 数据转换const userSchema = new Schema({ birthDate: Date});userSchema.virtual('age').get(function() { const today = new Date(); const birthDate = new Date(this.birthDate); let age = today.getFullYear() - birthDate.getFullYear(); const m = today.getMonth() - birthDate.getMonth(); if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { age--; } return age;});3. 关联查询const commentSchema = new Schema({ text: String, author: { type: Schema.Types.ObjectId, ref: 'User' }, post: { type: Schema.Types.ObjectId, ref: 'Post' }});// 在 Post 模型上添加虚拟字段获取评论postSchema.virtual('comments', { ref: 'Comment', localField: '_id', foreignField: 'post'});最佳实践用于计算和格式化:虚拟字段适合用于计算值和格式化显示避免复杂计算:复杂的计算可能影响性能合理使用关联:虚拟字段关联可以简化查询逻辑配置 JSON 输出:根据需要配置 toJSON 和 toObject文档清晰:为虚拟字段添加清晰的注释说明其用途
阅读 0·2月22日 20:07

Mongoose 如何管理连接和处理错误?

Mongoose 连接管理和错误处理是构建稳定应用的关键部分。正确处理连接状态和错误可以确保应用的可靠性。连接管理基本连接const mongoose = require('mongoose');// 基本连接mongoose.connect('mongodb://localhost:27017/mydb');// 带选项的连接mongoose.connect('mongodb://localhost:27017/mydb', { useNewUrlParser: true, useUnifiedTopology: true, maxPoolSize: 100, serverSelectionTimeoutMS: 5000, socketTimeoutMS: 45000});连接事件监听// 连接成功mongoose.connection.on('connected', () => { console.log('Mongoose connected to MongoDB');});// 连接错误mongoose.connection.on('error', (err) => { console.error('Mongoose connection error:', err);});// 连接断开mongoose.connection.on('disconnected', () => { console.log('Mongoose disconnected');});// 连接关闭mongoose.connection.on('close', () => { console.log('Mongoose connection closed');});连接状态检查// 检查连接状态console.log(mongoose.connection.readyState);// 0 = disconnected// 1 = connected// 2 = connecting// 3 = disconnecting// 辅助函数function isConnected() { return mongoose.connection.readyState === 1;}function isConnecting() { return mongoose.connection.readyState === 2;}错误处理连接错误处理// 使用 try-catch 处理连接错误async function connectToDatabase() { try { await mongoose.connect('mongodb://localhost:27017/mydb'); console.log('Connected to MongoDB'); } catch (error) { console.error('Failed to connect to MongoDB:', error); process.exit(1); }}// 使用 Promise.catchmongoose.connect('mongodb://localhost:27017/mydb') .then(() => console.log('Connected')) .catch(err => console.error('Connection error:', err));查询错误处理// 查询错误处理async function findUser(userId) { try { const user = await User.findById(userId); if (!user) { throw new Error('User not found'); } return user; } catch (error) { if (error.name === 'CastError') { console.error('Invalid user ID format'); } else if (error.name === 'MongooseError') { console.error('Mongoose error:', error.message); } else { console.error('Unexpected error:', error); } throw error; }}验证错误处理// 验证错误处理async function createUser(userData) { try { const user = await User.create(userData); return user; } catch (error) { if (error.name === 'ValidationError') { const errors = {}; Object.keys(error.errors).forEach(key => { errors[key] = error.errors[key].message; }); console.error('Validation errors:', errors); throw { message: 'Validation failed', errors }; } throw error; }}重复键错误处理// 重复键错误处理async function createUniqueUser(userData) { try { const user = await User.create(userData); return user; } catch (error) { if (error.code === 11000) { const field = Object.keys(error.keyPattern)[0]; const value = error.keyValue[field]; console.error(`Duplicate key error: ${field} = ${value}`); throw { message: `${field} already exists`, field }; } throw error; }}重连机制自动重连// Mongoose 默认会自动重连mongoose.connect('mongodb://localhost:27017/mydb', { // 自动重连配置 autoReconnect: true, reconnectTries: Number.MAX_VALUE, reconnectInterval: 1000});自定义重连逻辑let reconnectAttempts = 0;const maxReconnectAttempts = 5;mongoose.connection.on('disconnected', () => { if (reconnectAttempts < maxReconnectAttempts) { reconnectAttempts++; console.log(`Attempting to reconnect (${reconnectAttempts}/${maxReconnectAttempts})...`); setTimeout(() => { mongoose.connect('mongodb://localhost:27017/mydb'); }, 1000 * reconnectAttempts); } else { console.error('Max reconnection attempts reached'); process.exit(1); }});连接池管理连接池配置mongoose.connect('mongodb://localhost:27017/mydb', { // 连接池配置 maxPoolSize: 100, // 最大连接数 minPoolSize: 10, // 最小连接数 maxIdleTimeMS: 30000, // 最大空闲时间 waitQueueTimeoutMS: 5000 // 等待队列超时});监控连接池// 监控连接池状态setInterval(() => { const poolStatus = { ready: mongoose.connection.client.topology.s.pool.totalConnectionCount, active: mongoose.connection.client.topology.s.pool.activeConnectionCount, idle: mongoose.connection.client.topology.s.pool.idleConnectionCount }; console.log('Connection pool status:', poolStatus);}, 60000);优雅关闭优雅关闭处理async function gracefulShutdown() { console.log('Shutting down gracefully...'); try { // 关闭数据库连接 await mongoose.connection.close(); console.log('MongoDB connection closed'); // 退出进程 process.exit(0); } catch (error) { console.error('Error during shutdown:', error); process.exit(1); }}// 监听进程退出信号process.on('SIGTERM', gracefulShutdown);process.on('SIGINT', gracefulShutdown);最佳实践始终处理连接错误:不要忽略连接错误使用连接池:配置合适的连接池大小实现重连机制:确保应用能从连接中断中恢复优雅关闭:正确处理应用关闭时的连接清理监控连接状态:定期检查连接健康状态错误分类处理:根据错误类型采取不同的处理策略日志记录:记录连接和错误信息以便调试超时配置:设置合理的超时时间避免长时间等待
阅读 0·2月22日 20:06

Mongoose 如何与 TypeScript 结合使用?

Mongoose 与 TypeScript 结合使用可以提供类型安全、更好的开发体验和代码提示。通过使用 Mongoose 的类型定义,可以在编译时捕获错误。基本类型定义定义 Schema 类型import mongoose, { Schema, Document, Model } from 'mongoose';// 定义文档接口interface IUser extends Document { name: string; email: string; age: number; createdAt: Date;}// 定义 Schemaconst userSchema: Schema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true, unique: true }, age: { type: Number, min: 0 }, createdAt: { type: Date, default: Date.now }});// 创建模型类型interface IUserModel extends Model<IUser> { findByEmail(email: string): Promise<IUser | null>;}// 创建模型const User: IUserModel = mongoose.model<IUser, IUserModel>('User', userSchema);使用类型化模型创建文档const user: IUser = new User({ name: 'John Doe', email: 'john@example.com', age: 25});await user.save();查询文档// 查询单个文档const user: IUser | null = await User.findById(userId);// 查询多个文档const users: IUser[] = await User.find({ age: { $gte: 18 } });// 使用 lean() 返回普通对象const plainUsers = await User.find().lean();更新文档const user: IUser | null = await User.findById(userId);if (user) { user.age = 26; await user.save();}// 使用 findOneAndUpdateconst updatedUser: IUser | null = await User.findOneAndUpdate( { email: 'john@example.com' }, { age: 26 }, { new: true });静态方法类型定义静态方法interface IUserModel extends Model<IUser> { findByEmail(email: string): Promise<IUser | null>; findAdults(): Promise<IUser[]>; countByAge(minAge: number): Promise<number>;}// 实现静态方法userSchema.statics.findByEmail = function(email: string): Promise<IUser | null> { return this.findOne({ email });};userSchema.statics.findAdults = function(): Promise<IUser[]> { return this.find({ age: { $gte: 18 } });};userSchema.statics.countByAge = function(minAge: number): Promise<number> { return this.countDocuments({ age: { $gte: minAge } });};// 使用静态方法const user = await User.findByEmail('john@example.com');const adults = await User.findAdults();const count = await User.countByAge(18);实例方法类型定义实例方法interface IUser extends Document { name: string; email: string; age: number; getFullName(): string; isAdult(): boolean; updateAge(newAge: number): Promise<IUser>;}// 实现实例方法userSchema.methods.getFullName = function(): string { return this.name;};userSchema.methods.isAdult = function(): boolean { return this.age >= 18;};userSchema.methods.updateAge = async function(newAge: number): Promise<IUser> { this.age = newAge; return this.save();};// 使用实例方法const user = await User.findById(userId);if (user) { console.log(user.getFullName()); console.log(user.isAdult()); await user.updateAge(26);}虚拟字段类型定义虚拟字段interface IUser extends Document { firstName: string; lastName: string; fullName: string; // 虚拟字段}const userSchema: Schema = new Schema({ firstName: { type: String, required: true }, lastName: { type: String, required: true }});// 定义虚拟字段userSchema.virtual('fullName').get(function(this: IUser): string { return `${this.firstName} ${this.lastName}`;});userSchema.virtual('fullName').set(function(this: IUser, value: string): void { const parts = value.split(' '); this.firstName = parts[0]; this.lastName = parts[1];});嵌套文档类型定义嵌套 Schemainterface IAddress { street: string; city: string; state: string; zipCode: string;}interface IUser extends Document { name: string; email: string; address: IAddress;}const addressSchema: Schema = new Schema({ street: { type: String, required: true }, city: { type: String, required: true }, state: { type: String, required: true }, zipCode: { type: String, required: true }});const userSchema: Schema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true }, address: { type: addressSchema, required: true }});数组字段类型定义数组类型interface IUser extends Document { name: string; tags: string[]; scores: number[];}const userSchema: Schema = new Schema({ name: { type: String, required: true }, tags: [String], scores: [Number]});关联类型定义关联interface IPost extends Document { title: string; content: string; author: mongoose.Types.ObjectId;}interface IUser extends Document { name: string; email: string; posts: mongoose.Types.ObjectId[];}const postSchema: Schema = new Schema({ title: { type: String, required: true }, content: { type: String, required: true }, author: { type: Schema.Types.ObjectId, ref: 'User' }});const userSchema: Schema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true }, posts: [{ type: Schema.Types.ObjectId, ref: 'Post' }]});// 使用 populateconst user = await User.findById(userId).populate('posts');中间件类型定义中间件import { HookNextFunction } from 'mongoose';// Pre 中间件userSchema.pre('save', function(this: IUser, next: HookNextFunction): void { this.email = this.email.toLowerCase(); next();});// Post 中间件userSchema.post('save', function(this: IUser, doc: IUser): void { console.log('User saved:', doc.email);});// 异步中间件userSchema.pre('save', async function(this: IUser, next: HookNextFunction): Promise<void> { if (await emailExists(this.email)) { return next(new Error('Email already exists')); } next();});async function emailExists(email: string): Promise<boolean> { const count = await User.countDocuments({ email }); return count > 0;}泛型模型创建泛型模型interface BaseModel extends Document { createdAt: Date; updatedAt: Date;}function createTimestampedModel<T extends Document>( name: string, schema: Schema): Model<T & BaseModel> { schema.add({ createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); schema.pre('save', function(this: T & BaseModel, next: HookNextFunction): void { this.updatedAt = new Date(); next(); }); return mongoose.model<T & BaseModel>(name, schema);}// 使用泛型模型interface IUser extends Document { name: string; email: string;}const userSchema: Schema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true }});const User = createTimestampedModel<IUser>('User', userSchema);最佳实践使用接口定义类型:为所有 Schema 定义接口严格类型检查:启用 TypeScript 的严格模式避免 any 类型:尽量使用具体类型使用类型断言:在必要时使用类型断言文档完善:为类型添加清晰的注释测试覆盖:为类型化模型编写测试使用工具:利用 TypeScript 的类型提示和自动完成
阅读 0·2月22日 20:06

Session在TensorFlow 1.x中的作用是什么?TensorFlow 2.x为什么取消了Session?

在深度学习框架的发展历程中,TensorFlow 1.x与2.x的演进代表了计算模型执行模式的显著转变。Session机制作为TensorFlow 1.x的核心组件,曾是管理计算图执行的关键,但其在TensorFlow 2.x中被彻底移除,这引发了开发者关于架构设计哲学的广泛讨论。本文将深入剖析Session在1.x中的技术角色,以及2.x为何选择弃用它,同时提供可落地的迁移实践建议。通过理解这一变化,开发者能更好地适应TensorFlow 2.x的现代化开发范式,避免遗留代码的兼容性陷阱。Session在TensorFlow 1.x中的作用核心职责与技术原理TensorFlow 1.x采用静态计算图(Static Computation Graph)模型,所有操作(如张量运算)需先构建图结构,再通过Session进行执行。Session的核心作用包括:图管理:创建Session实例后,框架自动初始化计算图的全局状态,包括变量、操作等资源的分配。执行控制:Session提供run()方法,将计算图分块执行,并处理依赖关系(如变量初始化)。例如,变量需在Session中显式运行tf.global_variables_initializer()。资源隔离:多Session支持并行执行不同计算图,避免资源冲突,适用于分布式训练场景。此模式源于早期硬件限制(如GPU内存管理),通过图优化(如tf.graph_util.remove_ctrl_dependencies)提升性能,但引入了运行时开销——每次调用run()需遍历图结构,导致调试和迭代效率低下。代码示例:1.x中的Session实践以下展示Session在1.x中运行计算图的典型用法:import tensorflow as tf# 构建静态计算图a = tf.constant(2)b = tf.constant(3)c = a + b# 创建Session并执行with tf.Session() as sess: # 初始化全局变量(可选,但常见) sess.run(tf.global_variables_initializer()) # 执行计算并获取结果 result = sess.run(c) print(f"计算结果: {result}")关键点:Session强制显式调用run(),使代码流程与计算执行耦合。开发者需手动管理图生命周期(如tf.reset_default_graph()),易引发内存泄漏或图冲突问题。TensorFlow 2.x为什么取消了Session?从Eager Execution到动态计算TensorFlow 2.x通过Eager Execution(即时执行)彻底改变了设计哲学:动态计算图:操作在运行时立即执行,无需预构建静态图。例如,a = tf.constant(2)直接创建张量,而非存储在图中。Session的冗余:Session在1.x中用于显式触发计算,但在2.x中,Eager Execution使计算在Python层面直接执行,Session成为不必要的封装。核心原因:开发效率提升:Eager Execution支持Python原生调试(如print()、breakpoint()),简化迭代过程。API简化:移除Session后,代码更接近NumPy风格,降低学习门槛(例如,直接调用.numpy()获取张量值)。硬件抽象:Eager Execution自动处理设备分配(CPU/GPU),避免1.x中手动指定设备的复杂性。TensorFlow团队在官方文档中明确指出:"Eager Execution enables interactive use, making TensorFlow more accessible for beginners and researchers." 这一转变源于2017年TensorFlow 2.0的发布,Session被标记为遗留API,并在2.0后逐步弃用。代码对比:1.x vs 2.x1.x Session代码(需显式Session)import tensorflow as tf# 传统1.x模式a = tf.constant(2)b = tf.constant(3)with tf.Session() as sess: c = sess.run(a + b) print(c)2.x Eager Execution代码(Session隐式移除)import tensorflow as tf# 2.x模式:直接执行,无需Sessiona = tf.constant(2)b = tf.constant(3)c = a + bprint(c.numpy()) # 直接获取结果差异分析:在2.x中,tf.add()等操作自动执行,无需run()或Session。若需显式图控制,可通过tf.function(如@tf.function装饰器)转换为静态图,但默认场景下Session已无存在必要。迁移实践建议从1.x到2.x的平滑过渡若遗留1.x代码需迁移到2.x,遵循以下步骤:启用Eager Execution(默认已启用):import tensorflow as tftf.enable_eager_execution() # TensorFlow 1.x兼容模式,但2.x中无需此行重构Session代码:将显式Session.run()替换为直接操作(如c.numpy())。使用tf.keras API替代1.x的tf.Session:例如,Keras模型直接调用model.predict()。处理全局变量:1.x中tf.global_variables_initializer()在2.x中被tf.Variable自动管理,无需显式调用。代码示例:# 1.x方式var = tf.Variable(0)sess.run(var.assign(5))# 2.x方式(直接赋值)var = tf.Variable(0)var.assign(5) # 返回新张量调试技巧:利用tf.debugging.check_numerics()检测数值异常。在Jupyter中使用%tensorflow_version 1.x切换模式,但推荐始终使用2.x以获益于Eager Execution。常见陷阱与规避策略性能问题:Eager Execution在CPU上可能较慢,但GPU自动优化。对高性能需求场景,使用tf.functionjit编译(如@tf.function)以恢复1.x性能。兼容性:1.x中Session依赖的tf.Session在2.x中已弃用,调用将抛出RuntimeError,需更新代码。最佳实践:避免在2.x中滥用Session——它会强制静态图,与Eager Execution理念冲突。仅在特定场景(如分布式训练)需回退到1.x模式,但推荐使用tf.distribute库。结论Session在TensorFlow 1.x中是管理静态计算图的必要机制,但其在2.x中的取消并非技术倒退,而是架构设计的成熟体现。TensorFlow 2.x通过Eager Execution将计算模型推向更直观、高效的动态执行范式,显著提升了开发体验和可维护性。对于开发者而言,理解Session的淘汰原因并积极拥抱Eager Execution,是适应现代深度学习生态的关键。同时,通过tf.function等工具,可灵活平衡动态与静态执行的优势,确保代码在2.x中既简洁又高性能。未来,TensorFlow将持续优化Eager Execution,使其成为标准开发实践。​
阅读 0·2月22日 17:48