模式(Schemas)

优质
小牛编辑
135浏览
2023-12-01

建议先阅读快速上手,简单了解一下Mongoose的工作流程。 如果你要从4.x迁移到5.x,请阅读迁移指引.

定义一个schema

Mongoose 的一切始于 Schema。每个 schema 都会映射到一个 MongoDB collection ,并定义这个collection里的文档的构成。

  var mongoose = require('mongoose');
  var Schema = mongoose.Schema;

  var blogSchema = new Schema({
    title:  String,
    author: String,
    body:   String,
    comments: [{ body: String, date: Date }],
    date: { type: Date, default: Date.now },
    hidden: Boolean,
    meta: {
      votes: Number,
      favs:  Number
    }
  });

在这之后你还想添加 keys 的话, 请使用 Schema#add 方法。

document 里每个属性的类型都会被转换为 在 blogSchema 里定义对应的 SchemaType。 例如 title 属性会被转换为 SchemaType String, 而 date属性会被转换为 SchemaType Date。 还可以像上面 meta 属性,更详细地指定嵌套在里面的属性类型。

允许使用的 SchemaTypes 有:

  • String
  • Number
  • Date
  • Buffer
  • Boolean
  • Mixed
  • ObjectId
  • Array

更多关于 SchemaTypes

Schema的功能不只是定义文档结构和属性类型,它可以定义——

  • document 的 instance methods
  • model 的 static Model methods
  • 复合索引
  • 文档的生命周期钩子,也成为中间件

创建一个 model

我们要把 schema 转换为一个 Model, 使用 mongoose.model(modelName, schema) 函数:

  var Blog = mongoose.model('Blog', blogSchema);
  // ready to go!

实例方法(method)

documents 是 Models 的实例。 Document 有很多自带的实例方法, 当然也可以自定义我们自己的方法。

  // define a schema
  var animalSchema = new Schema({ name: String, type: String });

  // assign a function to the "methods" object of our animalSchema
  animalSchema.methods.findSimilarTypes = function(cb) {
    return this.model('Animal').find({ type: this.type }, cb);
  };

现在所有 animal 实例都有 findSimilarTypes 方法:

  var Animal = mongoose.model('Animal', animalSchema);
  var dog = new Animal({ type: 'dog' });

  dog.findSimilarTypes(function(err, dogs) {
    console.log(dogs); // woof
  });
  • 重写 mongoose 的默认方法会造成无法预料的结果,相关链接。
  • 不要在自定义方法中使用 ES6 箭头函数,会造成 this 指向错误。

静态方法(static)

添加 Model 的静态方法也十分简单,继续用 animalSchema 举例:

  // assign a function to the "statics" object of our animalSchema
  animalSchema.statics.findByName = function(name, cb) {
    return this.find({ name: new RegExp(name, 'i') }, cb);
  };

  var Animal = mongoose.model('Animal', animalSchema);
  Animal.findByName('fido', function(err, animals) {
    console.log(animals);
  });

同样不要在静态方法中使用 ES6 箭头函数

查询助手(query helper)

查询助手作用于 query 实例,方便你自定义拓展你的 链式查询

  animalSchema.query.byName = function(name) {
    return this.find({ name: new RegExp(name, 'i') });
  };

  var Animal = mongoose.model('Animal', animalSchema);
  Animal.find().byName('fido').exec(function(err, animals) {
    console.log(animals);
  });

索引(index)

MongoDB 支持 secondary indexes. 在 mongoose 中,我们在 Schema 中定义索引。索引分字段级别和schema级别, 复合索引 需要在 schema 级别定义。

  var animalSchema = new Schema({
    name: String,
    type: String,
    tags: { type: [String], index: true } // field level
  });

  animalSchema.index({ name: 1, type: -1 }); // schema level

应用启动时, Mongoose 会自动调用 createIndex 初始化你定义的索引。 Mongoose 顺序处理每一个 createIndex ,然后在model触发 'index' 事件。 While nice for development, it is recommended this behavior be disabled in production since index creation can cause a significant performance impact. Disable the behavior by setting the autoIndex option of your schema to false, or globally on the connection by setting the option autoIndex to false.

  mongoose.connect('mongodb://user:pass@localhost:port/database', { autoIndex: false });
  // or
  mongoose.createConnection('mongodb://user:pass@localhost:port/database', { autoIndex: false });
  // or
  animalSchema.set('autoIndex', false);
  // or
  new Schema({..}, { autoIndex: false });

索引构建完成或失败后,Mongoose 会触发 index 事件。

  // Will cause an error because mongodb has an _id index by default that
  // is not sparse
  animalSchema.index({ _id: 1 }, { sparse: true });
  var Animal = mongoose.model('Animal', animalSchema);

  Animal.on('index', function(error) {
    // "_id index cannot be sparse"
    console.log(error.message);
  });

相关链接 Model#ensureIndexes

虚拟值(Virtual)

Virtuals 是 document 的属性,但是不会被保存到 MongoDB。 getter 可以用于格式化和组合字段数据, setter 可以很方便地分解一个值到多个字段。

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

  // compile our model
  var Person = mongoose.model('Person', personSchema);

  // create a document
  var axl = new Person({
    name: { first: 'Axl', last: 'Rose' }
  });

如果你要log出全名,可以这么做:

console.log(axl.name.first + ' ' + axl.name.last); // Axl Rose

但是每次都这么拼接实在太麻烦了, 推荐你使用virtual property getter, 这个方法允许你定义一个 fullName 属性,但不必保存到数据库。

personSchema.virtual('fullName').get(function () {
  return this.name.first + ' ' + this.name.last;
});

现在, mongoose 可以调用 getter 函数访问 fullName 属性:

console.log(axl.fullName); // Axl Rose

如果对 document 使用 toJSON()toObject(),默认不包括虚拟值, 你需要额外向 toObject() 或者 toJSON() 传入参数 { virtuals: true }

你也可以设定虚拟值的 setter ,下例中,当你赋值到虚拟值时,它可以自动拆分到其他属性:

personSchema.virtual('fullName').
  get(function() { return this.name.first + ' ' + this.name.last; }).
  set(function(v) {
    this.name.first = v.substr(0, v.indexOf(' '));
    this.name.last = v.substr(v.indexOf(' ') + 1);
  });

axl.fullName = 'William Rose'; // Now `axl.name.first` is "William"

再次强调,虚拟值不能用于查询和字段选择,因为虚拟值不储存于 MongoDB。

别名(Alias)

Aliase 是一种特殊的虚拟值,它的 getter 和 setter 会无缝链接到另一个值。这是一个节省带宽的做法, 你可以储存一个更短的属性名到数据库,同时在调用的时候保持可读性。

var personSchema = new Schema({
  n: {
    type: String,
    // Now accessing `name` will get you the value of `n`, and setting `n` will set the value of `name`
    alias: 'name'
  }
});

// Setting `name` will propagate to `n`
var person = new Person({ name: 'Val' });
console.log(person); // { n: 'Val' }
console.log(person.toObject({ virtuals: true })); // { n: 'Val', name: 'Val' }
console.log(person.name); // "Val"

person.name = 'Not Val';
console.log(person); // { n: 'Not Val' }

选项

Schemas 有很多可配置选项,你可以在构造时传入或者直接 set

new Schema({..}, options);

// or

var schema = new Schema({..});
schema.set(option, value);

Valid options:

  • autoIndex
  • bufferCommands
  • capped
  • collection
  • id
  • _id
  • minimize
  • read
  • shardKey
  • strict
  • strictQuery
  • toJSON
  • toObject
  • typeKey
  • validateBeforeSave
  • versionKey
  • collation
  • skipVersioning
  • timestamps
  • useNestedStrict

option: autoIndex

应用启动时,Mongoose 自动发送 createIndex 指令, schema 里的每个 index 都会被创建。 如果你需要关闭自动创建功能或者需要在创建后进行一系列操作, 可以把 autoIndex 设为 false, 然后对 model 调用 ensureIndexes:

var schema = new Schema({..}, { autoIndex: false });
var Clock = mongoose.model('Clock', schema);
Clock.ensureIndexes(callback);

option: bufferCommands

By default, mongoose buffers commands when the connection goes down until the driver manages to reconnect. To disable buffering, set bufferCommands to false.

var schema = new Schema({..}, { bufferCommands: false });

The schema bufferCommands option overrides the global bufferCommands option.

mongoose.set('bufferCommands', true);
// Schema option below overrides the above, if the schema option is set.
var schema = new Schema({..}, { bufferCommands: false });

option: capped

Mongoose 支持 MongoDB 的 capped collections。 要从底层把 collection 设定为 capped (封顶), 可以把 collection 的最大容量设定到 capped 选项(单位bytes)。

new Schema({..}, { capped: 1024 });

The capped option may also be set to an object if you want to pass additional options like max or autoIndexId. 这个情况下你需要显式传入必要值 size

new Schema({..}, { capped: { size: 1024, max: 1000, autoIndexId: true } });

option: collection

Mongoose 通过 utils.toCollectionName 方法 默认生成 collection 的名称(生成 model 名称的复数形式)。 设置这个选项可以自定义名称。

var dataSchema = new Schema({..}, { collection: 'data' });

option: id

Mongoose 会默认生成一个虚拟值 id,指向文档的 _id 字段。 如果你不需要 id 虚拟值,可以通过这个选项禁用此功能。

// default behavior
var schema = new Schema({ name: String });
var Page = mongoose.model('Page', schema);
var p = new Page({ name: 'mongodb.org' });
console.log(p.id); // '50341373e894ad16347efe01'

// disabled id
var schema = new Schema({ name: String }, { id: false });
var Page = mongoose.model('Page', schema);
var p = new Page({ name: 'mongodb.org' });
console.log(p.id); // undefined

option: _id

Mongoose 默认给你的 Schema 赋值一个 _id。 这个值的类型是 ObjectId,这与MongoDB的默认表现一致。 如果你不需要 _id,可以通过这个选项禁用此功能。

此选项只能用于 subdocument。 Mongoose 不能保存没有id的文档,如果你硬是要这么做,会报错的哦。

// default behavior
var schema = new Schema({ name: String });
var Page = mongoose.model('Page', schema);
var p = new Page({ name: 'mongodb.org' });
console.log(p); // { _id: '50341373e894ad16347efe01', name: 'mongodb.org' }

// disabled _id
var childSchema = new Schema({ name: String }, { _id: false });
var parentSchema = new Schema({ children: [childSchema] });

var Model = mongoose.model('Model', parentSchema);

Model.create({ children: [{ name: 'Luke' }] }, function(error, doc) {
  // doc.children[0]._id will be undefined
});

option: minimize

Mongoose 默认不保存空对象。

var schema = new Schema({ name: String, inventory: {} });
var Character = mongoose.model('Character', schema);

// will store `inventory` field if it is not empty
var frodo = new Character({ name: 'Frodo', inventory: { ringOfPower: 1 }});
Character.findOne({ name: 'Frodo' }, function(err, character) {
  console.log(character); // { name: 'Frodo', inventory: { ringOfPower: 1 }}
});

// will not store `inventory` field if it is empty
var sam = new Character({ name: 'Sam', inventory: {}});
Character.findOne({ name: 'Sam' }, function(err, character) {
  console.log(character); // { name: 'Sam' }
});

如果把 minimize 选项设为 false,Mongoose 将保存空对象。

var schema = new Schema({ name: String, inventory: {} }, { minimize: false });
var Character = mongoose.model('Character', schema);

// will store `inventory` if empty
var sam = new Character({ name: 'Sam', inventory: {}});
Character.findOne({ name: 'Sam' }, function(err, character) {
  console.log(character); // { name: 'Sam', inventory: {}}
});

option: read

Allows setting query#read options at the schema level, providing us a way to apply default ReadPreferences to all queries derived from a model.

var schema = new Schema({..}, { read: 'primary' });            // also aliased as 'p'
var schema = new Schema({..}, { read: 'primaryPreferred' });   // aliased as 'pp'
var schema = new Schema({..}, { read: 'secondary' });          // aliased as 's'
var schema = new Schema({..}, { read: 'secondaryPreferred' }); // aliased as 'sp'
var schema = new Schema({..}, { read: 'nearest' });            // aliased as 'n'

The alias of each pref is also permitted so instead of having to type out 'secondaryPreferred' and getting the spelling wrong, we can simply pass 'sp'.

The read option also allows us to specify tag sets. These tell the driver from which members of the replica-set it should attempt to read. Read more about tag sets here and here.

NOTE: you may also specify the driver read pref strategy option when connecting:

// pings the replset members periodically to track network latency
var options = { replset: { strategy: 'ping' }};
mongoose.connect(uri, options);

var schema = new Schema({..}, { read: ['nearest', { disk: 'ssd' }] });
mongoose.model('JellyBean', schema);

option: shardKey

分片相关 The shardKey option is used when we have a sharded MongoDB architecture. Each sharded collection is given a shard key which must be present in all insert/update operations. We just need to set this schema option to the same shard key and we’ll be all set.

new Schema({ .. }, { shardKey: { tag: 1, name: 1 }})

Note that Mongoose does not send the shardcollection command for you. You must configure your shards yourself.

option: strict

Strict 选项默认为 true,这意味着你不能 save schema 里没有声明的属性。

var thingSchema = new Schema({..})
var Thing = mongoose.model('Thing', thingSchema);
var thing = new Thing({ iAmNotInTheSchema: true });
thing.save(); // iAmNotInTheSchema is not saved to the db

// set to false..
var thingSchema = new Schema({..}, { strict: false });
var thing = new Thing({ iAmNotInTheSchema: true });
thing.save(); // iAmNotInTheSchema is now saved to the db!!

doc.set() 也受该选项影响:

var thingSchema = new Schema({..})
var Thing = mongoose.model('Thing', thingSchema);
var thing = new Thing;
thing.set('iAmNotInTheSchema', true);
thing.save(); // iAmNotInTheSchema is not saved to the db

这个值可以在 model 级别重写,在第二个参数值传入:

var Thing = mongoose.model('Thing');
var thing = new Thing(doc, true);  // enables strict mode
var thing = new Thing(doc, false); // disables strict mode

The strict option may also be set to "throw" which will cause errors to be produced instead of dropping the bad data.

NOTE: Any key/val set on the instance that does not exist in your schema is always ignored, regardless of schema option.

var thingSchema = new Schema({..})
var Thing = mongoose.model('Thing', thingSchema);
var thing = new Thing;
thing.iAmNotInTheSchema = true;
thing.save(); // iAmNotInTheSchema is never saved to the db

option: strictQuery

For backwards compatibility, the strict option does not apply to the filter parameter for queries.

const mySchema = new Schema({ field: Number }, { strict: true });
const MyModel = mongoose.model('Test', mySchema);

// Mongoose will **not** filter out `notInSchema: 1`, despite `strict: true`
MyModel.find({ notInSchema: 1 });

The strict option does apply to updates.

// Mongoose will strip out `notInSchema` from the update if `strict` is
// not `false`
MyModel.updateMany({}, { $set: { notInSchema: 1 } });

Mongoose has a separate strictQuery option to toggle strict mode for the filter parameter to queries.

const mySchema = new Schema({ field: Number }, {
  strict: true,
  strictQuery: true // Turn on strict mode for query filters
});
const MyModel = mongoose.model('Test', mySchema);

// Mongoose will strip out `notInSchema: 1` because `strictQuery` is `true`
MyModel.find({ notInSchema: 1 });

option: toJSON

Exactly the same as the toObject option but only applies when the documents toJSON method is called.

var schema = new Schema({ name: String });
schema.path('name').get(function (v) {
  return v + ' is my name';
});
schema.set('toJSON', { getters: true, virtuals: false });
var M = mongoose.model('Person', schema);
var m = new M({ name: 'Max Headroom' });
console.log(m.toObject()); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom' }
console.log(m.toJSON()); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom is my name' }
// since we know toJSON is called whenever a js object is stringified:
console.log(JSON.stringify(m)); // { "_id": "504e0cd7dd992d9be2f20b6f", "name": "Max Headroom is my name" }

To see all available toJSON/toObject options, read this.

option: toObject

Documents 的 toObject 方法可以把文档转换成一个 plain javascript object (也就是去掉里面的方法)。 这是一个可以接收多个参数的方法,我们可以在 schemas 定义这些参数。

例如要打印出虚拟值,可以向 toObject 传入 { getters: true }

var schema = new Schema({ name: String });
schema.path('name').get(function (v) {
  return v + ' is my name';
});
schema.set('toObject', { getters: true });
var M = mongoose.model('Person', schema);
var m = new M({ name: 'Max Headroom' });
console.log(m); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom is my name' }

完整的 toObject 选项请看这里。

option: typeKey

By default, if you have an object with key 'type' in your schema, mongoose will interpret it as a type declaration.

// Mongoose interprets this as 'loc is a String'
var schema = new Schema({ loc: { type: String, coordinates: [Number] } });

However, for applications like geoJSON, the 'type' property is important. If you want to control which key mongoose uses to find type declarations, set the 'typeKey' schema option.

var schema = new Schema({
  // Mongoose interpets this as 'loc is an object with 2 keys, type and coordinates'
  loc: { type: String, coordinates: [Number] },
  // Mongoose interprets this as 'name is a String'
  name: { $type: String }
}, { typeKey: '$type' }); // A '$type' key means this object is a type declaration

option: validateBeforeSave

By default, documents are automatically validated before they are saved to the database. This is to prevent saving an invalid document. If you want to handle validation manually, and be able to save objects which don't pass validation, you can set validateBeforeSave to false.

var schema = new Schema({ name: String });
schema.set('validateBeforeSave', false);
schema.path('name').validate(function (value) {
    return v != null;
});
var M = mongoose.model('Person', schema);
var m = new M({ name: null });
m.validate(function(err) {
    console.log(err); // Will tell you that null is not allowed.
});
m.save(); // Succeeds despite being invalid

option: versionKey

versionKey 是 Mongoose 在文件创建时自动设定的。 这个值包含文件的内部修订号。 versionKey 是一个字符串,代表版本号的属性名, 默认值为 __v。如果这个值与你的计划冲突,你可以设定为其他名称:

var schema = new Schema({ name: 'string' });
var Thing = mongoose.model('Thing', schema);
var thing = new Thing({ name: 'mongoose v3' });
thing.save(); // { __v: 0, name: 'mongoose v3' }

// customized versionKey
new Schema({..}, { versionKey: '_somethingElse' })
var Thing = mongoose.model('Thing', schema);
var thing = new Thing({ name: 'mongoose v3' });
thing.save(); // { _somethingElse: 0, name: 'mongoose v3' }

你也可以赋值为 false 禁用 versionKey你不应该随便禁用这个功能,除非你清楚知道这有什么影响。

new Schema({..}, { versionKey: false });
var Thing = mongoose.model('Thing', schema);
var thing = new Thing({ name: 'no versioning please' });
thing.save(); // { name: 'no versioning please' }

option: collation

为 查询(query)和 聚合(aggregation)设置 collation。 这里有一份友好的 collation 指南。

var schema = new Schema({
  name: String
}, { collation: { locale: 'en_US', strength: 1 } });

var MyModel = db.model('MyModel', schema);

MyModel.create([{ name: 'val' }, { name: 'Val' }]).
  then(function() {
    return MyModel.find({ name: 'val' });
  }).
  then(function(docs) {
    // `docs` will contain both docs, because `strength: 1` means
    // MongoDB will ignore case when matching.
  });

option: skipVersioning

skipVersioning allows excluding paths from versioning (i.e., the internal revision will not be incremented even if these paths are updated). DO NOT do this unless you know what you're doing. For subdocuments, include this on the parent document using the fully qualified path.

new Schema({..}, { skipVersioning: { dontVersionMe: true } });
thing.dontVersionMe.push('hey');
thing.save(); // version is not incremented

option: timestamps

如果设置了 timestamps 选项, mongoose 会在你的 schema 自动添加 createdAtupdatedAt 字段, 其类型为 Date。

这两个字段的默认名是 createdAtupdatedAt, 你可以通过设定 timestamps.createdAttimestamps.updatedAt 自定义字段名称。

var thingSchema = new Schema({..}, { timestamps: { createdAt: 'created_at' } });
var Thing = mongoose.model('Thing', thingSchema);
var thing = new Thing();
thing.save(); // `created_at` & `updatedAt` will be included

option: useNestedStrict

在 mongoose4 中,update()findOneAndUpdate()只检查顶层 schema 的严格模式设定。

var childSchema = new Schema({}, { strict: false });
var parentSchema = new Schema({ child: childSchema }, { strict: 'throw' });
var Parent = mongoose.model('Parent', parentSchema);
Parent.update({}, { 'child.name': 'Luke Skywalker' }, function(error) {
  // 报错!原因是父Schema设定为`strict: throw`,但是因为只检查顶层,导致
  // 子Schema的 `strict: false` 遭到无情忽视
});

var update = { 'child.name': 'Luke Skywalker' };
var opts = { strict: false };
Parent.update({}, update, opts, function(error) {
  // 这样可以,因为重写了父Schema的 strict 选项
});

如果你把 useNestedStrict 设为 true,mongoose 就不会忽略嵌套的 strict 设定。

var childSchema = new Schema({}, { strict: false });
var parentSchema = new Schema({ child: childSchema },
  { strict: 'throw', useNestedStrict: true });
var Parent = mongoose.model('Parent', parentSchema);
Parent.update({}, { 'child.name': 'Luke Skywalker' }, function(error) {
  // Works!
});

Pluggable

Schemas 是 pluggable(可扩展的), 我们可以打包插件分享到社区,或者复用于自己的项目。

下一步

这章我们介绍了 Schemas,下一个章节将会介绍 SchemaTypes。