Mongoose中文文档-指南之验证(Validation)

 2018年11月27日    83     声明


验证器是定义于SchemaType中的验证中间件,用于文档更新或保存期间对输入值进行验证。你可以使用Mongoose内置的验证器,也可以自自定义验证器。无论哪种验证器,都可以手工或自动触发。

  1. 验证
  2. 内置验证器
  3. unique选项不是验证器
  4. 自定义验证器
  5. 异步自定义验证器
  6. 验证错误
  7. 嵌套对象上的requied验证器
  8. 更新验证器
  9. 更新验证器与this
  10. context选项
  11. 更新验证器仅在更新的路径上运行
  12. 更新验证程序仅在某些操作上运行
  13. $push$addToSet

1. 验证

在深入了解验证语法细节之前,应注意以下规则:

  • 验证定义于SchemaType
  • 验证是中间件,Mongoose默认在pre('save')中间件中注册验证。
  • 可以通过doc.validate(callback)doc.validateSync()手工执行验证
  • 验证器不会在undefined值上执行,仅required验证器除外
  • 验证是异步递归的; 当你调用Model#save时,也会执行子文档的验证。如果发生错误,Model#save回调会接收它
  • 验证是自定义的
var schema = new Schema({
  name: {
    type: String,
    required: true
  }
});
var Cat = db.model('Cat', schema);

// This cat has no name :(
var cat = new Cat();
cat.save(function(error) {
  assert.equal(error.errors['name'].message,
    'Path `name` is required.');

  error = cat.validateSync();
  assert.equal(error.errors['name'].message,
    'Path `name` is required.');
});


2. 内置验证器

Mongoose中有几个内置的验证器:

以上每个验证器链接都提供了关于如何启用以及自定义其错误消息的更多信息。

var breakfastSchema = new Schema({
  eggs: {
    type: Number,
    min: [6, 'Too few eggs'],
    max: 12
  },
  bacon: {
    type: Number,
    required: [true, 'Why no bacon?']
  },
  drink: {
    type: String,
    enum: ['Coffee', 'Tea'],
    required: function() {
      return this.bacon > 3;
    }
  }
});
var Breakfast = db.model('Breakfast', breakfastSchema);

var badBreakfast = new Breakfast({
  eggs: 2,
  bacon: 0,
  drink: 'Milk'
});
var error = badBreakfast.validateSync();
assert.equal(error.errors['eggs'].message,
  'Too few eggs');
assert.ok(!error.errors['bacon']);
assert.equal(error.errors['drink'].message,
  '`Milk` is not a valid enum value for path `drink`.');

badBreakfast.bacon = 5;
badBreakfast.drink = null;

error = badBreakfast.validateSync();
assert.equal(error.errors['drink'].message, 'Path `drink` is required.');

badBreakfast.bacon = null;
error = badBreakfast.validateSync();
assert.equal(error.errors['bacon'].message, 'Why no bacon?');


3. unique选项不是验证器

unique选项不是验证器,这是初学者常见的问题之一,它是构建MongoDB唯一索引的便捷帮助器。有关详细信息,请参阅FAQ

var uniqueUsernameSchema = new Schema({
  username: {
    type: String,
    unique: true
  }
});
var U1 = db.model('U1', uniqueUsernameSchema);
var U2 = db.model('U2', uniqueUsernameSchema);

var dup = [{ username: 'Val' }, { username: 'Val' }];
U1.create(dup, function(error) {
  // Race condition! This may save successfully, depending on whether
  // MongoDB built the index before writing the 2 docs.
});

// Need to wait for the index to finish building before saving,
// otherwise unique constraints may be violated.
U2.once('index', function(error) {
  assert.ifError(error);
  U2.create(dup, function(error) {
    // Will error, but will *not* be a mongoose validation error, it will be
    // a duplicate key error.
    assert.ok(error);
    assert.ok(!error.errors);
    assert.ok(error.message.indexOf('duplicate key error') !== -1);
  });
});

// There's also a promise-based equivalent to the event emitter API.
// The `init()` function is idempotent and returns a promise that
// will resolve once indexes are done building;
U2.init().then(function() {
  U2.create(dup, function(error) {
    // Will error, but will *not* be a mongoose validation error, it will be
    // a duplicate key error.
    assert.ok(error);
    assert.ok(!error.errors);
    assert.ok(error.message.indexOf('duplicate key error') !== -1);
  });
});


4. 自定义验证器

如果内置验证器不能满足需要,你还可以自定义验证器。

通过传入验证函数来声明自定义验证,你可以在SchemaType#validate() API文档中查看操作相关说明。

var userSchema = new Schema({
  phone: {
    type: String,
    validate: {
      validator: function(v) {
        return /\d{3}-\d{3}-\d{4}/.test(v);
      },
      message: props => `${props.value} is not a valid phone number!`
    },
    required: [true, 'User phone number required']
  }
});

var User = db.model('user', userSchema);
var user = new User();
var error;

user.phone = '555.0123';
error = user.validateSync();
assert.equal(error.errors['phone'].message,
  '555.0123 is not a valid phone number!');

user.phone = '';
error = user.validateSync();
assert.equal(error.errors['phone'].message,
  'User phone number required');

user.phone = '201-555-0123';
// Validation succeeds! Phone number is defined
// and fits `DDD-DDD-DDDD`
error = user.validateSync();
assert.equal(error, null);


5. 异步自定义验证器

自定义验证器可以是异步的。如果你的验证器函数返回一个Promise(像async函数),mongoose会等待这个promise解决。如果你更喜欢回调,需要设置isAsync选项,并且mongoose会将回调作为验证器函数的第二个参数传递。

var userSchema = new Schema({
  name: {
    type: String,
    // You can also make a validator async by returning a promise. If you
    // return a promise, do **not** specify the `isAsync` option.
    validate: function(v) {
      return new Promise(function(resolve, reject) {
        setTimeout(function() {
          resolve(false);
        }, 5);
      });
    }
  },
  phone: {
    type: String,
    validate: {
      isAsync: true,
      validator: function(v, cb) {
        setTimeout(function() {
          var phoneRegex = /\d{3}-\d{3}-\d{4}/;
          var msg = v + ' is not a valid phone number!';
          // First argument is a boolean, whether validator succeeded
          // 2nd argument is an optional error message override
          cb(phoneRegex.test(v), msg);
        }, 5);
      },
      // Default error message, overridden by 2nd argument to `cb()` above
      message: 'Default error message'
    },
    required: [true, 'User phone number required']
  }
});

var User = db.model('User', userSchema);
var user = new User();
var error;

user.phone = '555.0123';
user.name = 'test';
user.validate(function(error) {
  assert.ok(error);
  assert.equal(error.errors['phone'].message,
    '555.0123 is not a valid phone number!');
  assert.equal(error.errors['name'].message,
    'Validator failed for path `name` with value `test`');
});


6. 验证错误

验证失败后所返回的错误包含一个错误对象,其值为ValidatorError对象。每个ValidatorError都有kindpathvaluemessage属性。ValidatorError也可能有reason属性,如果验证程序中引发了错误,则此属性将包含所引发的错误。

var toySchema = new Schema({
  color: String,
  name: String
});

var validator = function(value) {
  return /red|white|gold/i.test(value);
};
toySchema.path('color').validate(validator,
  'Color `{VALUE}` not valid', 'Invalid color');
toySchema.path('name').validate(function(v) {
  if (v !== 'Turbo Man') {
    throw new Error('Need to get a Turbo Man for Christmas');
  }
  return true;
}, 'Name `{VALUE}` is not valid');

var Toy = db.model('Toy', toySchema);

var toy = new Toy({ color: 'Green', name: 'Power Ranger' });

toy.save(function (err) {
  // `err` is a ValidationError object
  // `err.errors.color` is a ValidatorError object
  assert.equal(err.errors.color.message, 'Color `Green` not valid');
  assert.equal(err.errors.color.kind, 'Invalid color');
  assert.equal(err.errors.color.path, 'color');
  assert.equal(err.errors.color.value, 'Green');

  // This is new in mongoose 5. If your validator throws an exception,
  // mongoose will use that message. If your validator returns `false`,
  // mongoose will use the 'Name `Power Ranger` is not valid' message.
  assert.equal(err.errors.name.message,
    'Need to get a Turbo Man for Christmas');
  assert.equal(err.errors.name.value, 'Power Ranger');
  // If your validator threw an error, the `reason` property will contain
  // the original error thrown, including the original stack trace.
  assert.equal(err.errors.name.reason.message,
    'Need to get a Turbo Man for Christmas');

  assert.equal(err.name, 'ValidationError');
});


7. 嵌套对象上的requied验证器

在mongoose中定义嵌套对象的验证器比较麻烦,因为嵌套对象不是完全确定的路径:

var personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
});

assert.throws(function() {
  // This throws an error, because 'name' isn't a full fledged path
  personSchema.path('name').required(true);
}, /Cannot.*'required'/);

// To make a nested object required, use a single nested schema
var nameSchema = new Schema({
  first: String,
  last: String
});

personSchema = new Schema({
  name: {
    type: nameSchema,
    required: true
  }
});

var Person = db.model('Person', personSchema);

var person = new Person();
var error = person.validateSync();
assert.ok(error.errors['name']);


8. 更新验证器

在上面的示例中,我们了解了文档验证。Mongoose还支持update()findOneAndUpdate操作的验证。默认情况下,更新验证程序处于关闭状态,可以通过runValidators选项来启用。

注意:默认情况下更新验证器是关闭的,因为它有几个警告。

var toySchema = new Schema({
  color: String,
  name: String
});

var Toy = db.model('Toys', toySchema);

Toy.schema.path('color').validate(function (value) {
  return /blue|green|white|red|orange|periwinkle/i.test(value);
}, 'Invalid color');

var opts = { runValidators: true };
Toy.updateOne({}, { color: 'bacon' }, opts, function (err) {
  assert.equal(err.errors.color.message,
    'Invalid color');
});


9. 更新验证器与this

更新验证器和文档验证器之间有一些关键差异。在上面的颜色验证功能中,this是指在使用文档验证时验证的文档。但是,在运行更新验证程序时,正在更新的文档可能不在服务器的内存中,因此默认情况下this的值为未定义。

var toySchema = new Schema({
  color: String,
  name: String
});

toySchema.path('color').validate(function(value) {
  // When running in `validate()` or `validateSync()`, the
  // validator can access the document using `this`.
  // Does **not** work with update validators.
  if (this.name.toLowerCase().indexOf('red') !== -1) {
    return value !== 'red';
  }
  return true;
});

var Toy = db.model('ActionFigure', toySchema);

var toy = new Toy({ color: 'red', name: 'Red Power Ranger' });
var error = toy.validateSync();
assert.ok(error.errors['color']);

var update = { color: 'red', name: 'Red Power Ranger' };
var opts = { runValidators: true };

Toy.updateOne({}, update, opts, function(error) {
  // The update validator throws an error:
  // "TypeError: Cannot read property 'toLowerCase' of undefined",
  // because `this` is **not** the document being updated when using
  // update validators
  assert.ok(error);
});


10. context选项

context选项使你可以将更新验证器中的this值设置为基础查询。

toySchema.path('color').validate(function(value) {
  // When running update validators with the `context` option set to
  // 'query', `this` refers to the query object.
  if (this.getUpdate().$set.name.toLowerCase().indexOf('red') !== -1) {
    return value === 'red';
  }
  return true;
});

var Toy = db.model('Figure', toySchema);

var update = { color: 'blue', name: 'Red Power Ranger' };
// Note the context option
var opts = { runValidators: true, context: 'query' };

Toy.updateOne({}, update, opts, function(error) {
  assert.ok(error.errors['color']);
});


11. 更新验证器仅在更新的路径上运行

更新验证器的另一个关键区别仅在会更新中所指定的路径上运行。如,在下面的示例中,由于在更新操作中未指定'name',因此更新验证将成功。

使用更新验证程序时,只有在尝试显式$unset设置键时,required验证器才会失败。

var kittenSchema = new Schema({
  name: { type: String, required: true },
  age: Number
});

var Kitten = db.model('Kitten', kittenSchema);

var update = { color: 'blue' };
var opts = { runValidators: true };
Kitten.updateOne({}, update, opts, function(err) {
  // Operation succeeds despite the fact that 'name' is not specified
});

var unset = { $unset: { name: 1 } };
Kitten.updateOne({}, unset, opts, function(err) {
  // Operation fails because 'name' is required
  assert.ok(err);
  assert.ok(err.errors['name']);
});


12. 更新验证程序仅在某些操作上运行

最后一个值得注意的细节,更新验证程序仅会在以下更新运算符上运行:

  • $set
  • $unset
  • $push (>= 4.8.0)
  • $addToSet (>= 4.8.0)
  • $pull (>= 4.12.0)
  • $pullAll (>= 4.12.0)

例如,无论number的值如何,以下更新都将成功,因为更新验证器会忽略$inc操作。

此外,$push$addToSet$pull$pullAll验证都不会对数组本身进行任何验证,只对数组的各个元素进行验证。

var testSchema = new Schema({
  number: { type: Number, max: 0 },
  arr: [{ message: { type: String, maxlength: 10 } }]
});

// Update validators won't check this, so you can still `$push` 2 elements
// onto the array, so long as they don't have a `message` that's too long.
testSchema.path('arr').validate(function(v) {
  return v.length < 2;
});

var Test = db.model('Test', testSchema);

var update = { $inc: { number: 1 } };
var opts = { runValidators: true };
Test.updateOne({}, update, opts, function(error) {
  // There will never be a validation error here
  update = { $push: [{ message: 'hello' }, { message: 'world' }] };
  Test.updateOne({}, update, opts, function(error) {
    // This will never error either even though the array will have at
    // least 2 elements.
  });
});


13. $push$addToSet

添加于 4.8.0更新验证器会在$push$addToSet上运行

var testSchema = new Schema({
  numbers: [{ type: Number, max: 0 }],
  docs: [{
    name: { type: String, required: true }
  }]
});

var Test = db.model('TestPush', testSchema);

var update = {
  $push: {
    numbers: 1,
    docs: { name: null }
  }
};
var opts = { runValidators: true };
Test.updateOne({}, update, opts, function(error) {
  assert.ok(error.errors['numbers']);
  assert.ok(error.errors['docs']);
});


变更记录

  • [2018-11-27] 基于Mongoose官方文档v5.3.12首次发布