Custom validators and error handling
Basic Concepts of Custom Validators
Mongoose allows for more granular data validation through custom validators. While built-in validators like required
and min
are commonly used, they often lack flexibility when dealing with complex business logic. Custom validators come in two forms: synchronous and asynchronous, and can be used directly in Schema definitions.
const userSchema = new mongoose.Schema({
phone: {
type: String,
validate: {
validator: function(v) {
return /^1[3456789]\d{9}$/.test(v);
},
message: props => `${props.value} is not a valid phone number`
}
}
});
Implementation of Synchronous Validators
Synchronous validators immediately return a boolean or error message, making them suitable for simple and fast validation. The validation function receives the current field value as a parameter and determines whether the validation passes based on the return value.
const productSchema = new mongoose.Schema({
price: {
type: Number,
validate: {
validator: function(value) {
// Price must be greater than cost price
return value > this.costPrice;
},
message: 'Selling price cannot be lower than cost price'
}
},
costPrice: Number
});
When validation fails, Mongoose adds the error to the document's errors collection. Synchronous validators are automatically executed before saving the document and can be manually triggered via doc.validate()
.
Use Cases for Asynchronous Validators
Validations requiring database queries or network requests must use asynchronous validators. By setting the isAsync
option to true, the validation function can return a Promise or use a callback function.
const orderSchema = new mongoose.Schema({
couponCode: {
type: String,
validate: {
validator: function(v) {
return new Promise((resolve, reject) => {
CouponModel.findOne({ code: v }, (err, coupon) => {
if (err || !coupon) return resolve(false);
resolve(coupon.isValid());
});
});
},
message: 'Invalid coupon code'
}
}
});
Asynchronous validations are automatically executed when saving a document. Note that they are not triggered by default during update operations and require explicit setting of runValidators: true
.
Error Handling Mechanisms for Validation
Mongoose provides multiple ways to handle errors. When validation fails, error messages are attached to the document and can be captured via the save
callback or Promise.
const newUser = new User({ phone: '123456' });
newUser.save()
.catch(err => {
console.log(err.errors['phone'].message); // Output specific error message
});
For batch operations, error handling requires special attention. When using Model.updateMany()
, validation failure for a single document does not prevent other documents from being updated, but an object containing error information will be returned.
Path-Level vs. Document-Level Validation
In addition to field-level validation, Mongoose also supports document-level validation. Complex logic involving multiple fields can be defined via the Schema's validate
method.
const bookingSchema = new mongoose.Schema({
startDate: Date,
endDate: Date,
guests: Number
});
bookingSchema.validate('datesValid', function() {
return this.endDate > this.startDate;
}, 'End date must be later than start date');
Document-level validation is executed before saving and can access all fields of the document. This approach is suitable for scenarios requiring cross-field validation, such as date ranges or quantity limits.
Advanced Usage of Custom Error Messages
Error messages can be dynamically generated, not just static strings. Through message templates in function form, contextual information about validation failures can be included.
const inventorySchema = new mongoose.Schema({
stock: {
type: Number,
validate: {
validator: function(v) {
return v >= 0;
},
message: props => `Stock value ${props.value} cannot be negative`
}
}
});
For multilingual applications, internationalization logic can be implemented in the message function. Error messages also support template variables, such as built-in placeholders like {PATH}
and {VALUE}
.
Combining Validators with Middleware
Validators are often used in combination with middleware to implement more comprehensive business logic. For example, additional validation can be performed in the pre('save')
hook, or validation logs can be recorded in the post('validate')
hook.
userSchema.pre('save', function(next) {
if (this.isModified('password')) {
if (this.password.length < 8) {
next(new Error('Password must be at least 8 characters'));
} else {
next();
}
} else {
next();
}
});
This combination maintains the centralization of validation logic while handling more complex business rules. Middleware can also be used to clean or standardize data before validation.
Strategies for Testing Custom Validators
Comprehensive testing is crucial for validation logic. Test cases should cover various edge cases, including valid inputs, extreme values, and intentionally constructed invalid inputs.
describe('User Model Validation', () => {
it('should reject invalid phone numbers', async () => {
const user = new User({ phone: '123' });
await expect(user.save()).to.eventually.be.rejectedWith('is not a valid phone number');
});
it('should accept valid phone numbers', async () => {
const user = new User({ phone: '13800138000' });
await expect(user.save()).to.eventually.be.fulfilled;
});
});
Using testing frameworks like Mocha+Chai makes it easy to verify validator behavior. For asynchronous validators, ensure tests properly handle Promises or callbacks.
Performance Optimization Considerations
Complex validation logic can impact performance, especially during batch operations. Several optimization strategies are worth considering:
- Add caching for frequently executed validations
- Move CPU-intensive validations to background processes
- Create in-memory caches for reference data that rarely changes
const couponCache = new Map();
couponSchema.path('code').validate({
validator: async function(code) {
if (couponCache.has(code)) {
return couponCache.get(code);
}
const valid = await checkCouponValidity(code);
couponCache.set(code, valid);
return valid;
},
message: 'Coupon has expired'
});
Reusability Patterns for Validation Logic
To avoid code duplication, common validation logic can be extracted into reusable components. Mongoose supports sharing validation logic through plugins.
// validators/phone.js
module.exports = function(schema, options) {
schema.path('phone').validate({
validator: function(v) {
return /^1[3456789]\d{9}$/.test(v);
},
message: 'Invalid phone number format'
});
};
// Usage in models
userSchema.plugin(require('./validators/phone'));
For more complex scenarios, validator factory functions can be created to generate different validation logic based on configuration. This approach is particularly suitable for scenarios where validation rules need to be adjusted according to different environments.
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:数据类型与字段验证