结构

如果你还没有这样做,请花一点时间阅读 快速开始 以了解 Mongoose 的工作原理。 如果你要从 6.x 迁移到 7.x,请花点时间阅读 迁移指南

定义你的结构

Mongoose 中的一切都始于结构。 每个结构都映射到一个 MongoDB 集合,并定义该集合中文档的形状。

import mongoose from 'mongoose';
const { Schema } = mongoose;

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

如果你想稍后添加其他键,请使用 Schema#add 方法。

代码 blogSchema 中的每个键都在文档中定义一个属性,该属性将被转换为其关联的 SchemaType。 例如,我们定义了属性 title 和属性 date,前者将转换为 字符串 SchemaType,后者将转换为 Date SchemaType。

请注意,如果属性仅需要类型,则可以使用简写符号来指定(将上面的 title 属性与 date 属性进行对比)。

还可以为键分配包含进一步键/类型定义的嵌套对象,如上面的 meta 属性。 只要键的值是不具有 type 属性的 POJO,就会发生这种情况。

在这些情况下,Mongoose 只为树中的叶子创建实际的结构路径。 (如上面的 meta.votesmeta.favs),并且分支没有实际路径。 这样做的一个副作用是上面的 meta 不能有自己的验证。 如果需要在树上进行验证,则需要在树上创建路径 - 有关如何执行此操作的更多信息,请参阅 子文档 部分。 另请阅读 SchemaTypes 指南的 混合 小节以了解一些问题。

允许的 SchemaType 是:

了解有关 此处的结构类型 的更多信息。

结构不仅定义文档的结构和属性转换,还定义文档 实例方法静态模型方法复合索引 和称为 middleware 的文档生命周期钩子。

创建模型

要使用我们的结构定义,我们需要将 blogSchema 转换为我们可以使用的 模型。 为此,我们将其传递到 mongoose.model(modelName, schema)

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

识别号

默认情况下,Mongoose 会向你的结构添加 _id 属性。

const schema = new Schema();

schema.path('_id'); // ObjectId { ... }

当你使用自动添加的 _id 属性创建新文档时,Mongoose 会为你的文档创建一个新的 ObjectId 类型的 _id

const Model = mongoose.model('Test', schema);

const doc = new Model();
doc._id instanceof mongoose.Types.ObjectId; // true

你还可以用你自己的 _id 覆盖 Mongoose 的默认 _id。 请小心: Mongoose 将拒绝保存没有 _id 的文档,因此如果你定义自己的 _id 路径,则你有责任设置 _id

const schema = new Schema({ _id: Number });
const Model = mongoose.model('Test', schema);

const doc = new Model();
await doc.save(); // Throws "document must have an _id before saving"

doc._id = 1;
await doc.save(); // works

实例方法

Models 的实例是 documents。 文档有很多自己的 内置实例方法。 我们还可以定义自己的自定义文档实例方法。

// define a schema
const animalSchema = new Schema({ name: String, type: String },
  {
  // Assign a function to the "methods" object of our animalSchema through schema options.
  // By following this approach, there is no need to create a separate TS type to define the type of the instance functions.
    methods: {
      findSimilarTypes(cb) {
        return mongoose.model('Animal').find({ type: this.type }, cb);
      }
    }
  });

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

现在我们所有的 animal 实例都有一个可用的 findSimilarTypes 方法。

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

dog.findSimilarTypes((err, dogs) => {
  console.log(dogs); // woof
});
  • 覆盖默认的 Mongoose 文档方法可能会导致不可预测的结果。 详细信息请参见 this
  • 上面的例子直接使用 Schema.methods 对象来保存实例方法。 你还可以按照 here 的描述使用 Schema.method() 辅助程序。
  • not 是否使用 ES6 箭头函数声明方法 (=>)。 箭头函数 明确阻止绑定 this,因此你的方法将 not 有权访问该文档,并且上面的示例将不起作用。

静态

你还可以向模型添加静态函数。 添加静态的等效方法有以下三种:

  • 将函数属性添加到结构构造函数的第二个参数 (statics)
  • schema.statics 添加函数属性
  • 调用 Schema#static() 功能

// define a schema
const animalSchema = new Schema({ name: String, type: String },
  {
  // Assign a function to the "statics" object of our animalSchema through schema options.
  // By following this approach, there is no need to create a separate TS type to define the type of the statics functions.
    statics: {
      findByName(name) {
        return this.find({ name: new RegExp(name, 'i') });
      }
    }
  });

// Or, Assign a function to the "statics" object of our animalSchema
animalSchema.statics.findByName = function(name) {
  return this.find({ name: new RegExp(name, 'i') });
};
// Or, equivalently, you can call `animalSchema.static()`.
animalSchema.static('findByBreed', function(breed) { return this.find({ breed }); });

const Animal = mongoose.model('Animal', animalSchema);
let animals = await Animal.findByName('fido');
animals = animals.concat(await Animal.findByBreed('Poodle'));

not 使用 ES6 箭头函数声明静态(=>)。 箭头函数为 明确阻止绑定 this,因此由于 this 的值,上述示例将不起作用。

查询助手

你还可以添加查询辅助函数,它们类似于实例方法,但用于 Mongoose 查询。 查询辅助程序方法让你可以扩展 mongoose 的 可链接查询构建器 API


// define a schema
const animalSchema = new Schema({ name: String, type: String },
  {
  // Assign a function to the "query" object of our animalSchema through schema options.
  // By following this approach, there is no need to create a separate TS type to define the type of the query functions.
    query: {
      byName(name) {
        return this.where({ name: new RegExp(name, 'i') });
      }
    }
  });

// Or, Assign a function to the "query" object of our animalSchema
animalSchema.query.byName = function(name) {
  return this.where({ name: new RegExp(name, 'i') });
};

const Animal = mongoose.model('Animal', animalSchema);

Animal.find().byName('fido').exec((err, animals) => {
  console.log(animals);
});

Animal.findOne().byName('fido').exec((err, animal) => {
  console.log(animal);
});

索引

MongoDB 支持 二级指标。 对于 mongoose,我们在 Schema at the path levelschema 级别中定义这些索引。 创建 复合索引 时,需要在结构级别定义索引。

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

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

其他索引选项请参见 SchemaType#index()

当你的应用启动时,Mongoose 会自动为结构中的每个定义的索引调用 createIndex。 Mongoose 将为每个索引依次调用 createIndex,并在所有 createIndex 调用成功或出现错误时在模型上发出 'index' 事件。 虽然有利于开发,但建议在生产中禁用此行为,因为索引创建可能会导致 显着的性能影响。 通过将结构的 autoIndex 选项设置为 false 来禁用该行为,或者通过将选项 autoIndex 设置为 false 在连接上全局禁用该行为。

mongoose.connect('mongodb://user:pass@127.0.0.1:port/database', { autoIndex: false });
// or
mongoose.createConnection('mongodb://user:pass@127.0.0.1:port/database', { autoIndex: false });
// or
mongoose.set('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 });
const Animal = mongoose.model('Animal', animalSchema);

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

另请参见 Model#ensureIndexes 方法。

虚拟

虚拟 是你可以获取和设置但不会持久保存到 MongoDB 的文档属性。 getter 对于格式化或组合字段很有用,而 setter 对于将单个值分解为多个值进行存储非常有用。

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

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

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

假设你想打印出该人的全名。 你可以自己做:

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

但每次都输入名字和姓氏 concatenating 会很麻烦。 如果你想对名称(例如 删除变音符号)进行一些额外处理怎么办? 虚拟属性获取者 允许你定义不会持久保存到 MongoDB 的 fullName 属性。

// That can be done either by adding it to schema options:
const personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
}, {
  virtuals: {
    fullName: {
      get() {
        return this.name.first + ' ' + this.name.last;
      }
    }
  }
});

// Or by using the virtual method as following:
personSchema.virtual('fullName').get(function() {
  return this.name.first + ' ' + this.name.last;
});

现在,每次你访问 fullName 属性时,Mongoose 都会调用你的 getter 函数:

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

如果你使用 toJSON()toObject() Mongoose 默认情况下将不包含虚拟机。 将 { virtuals: true } 传递到 toJSON()toObject() 以包含虚拟。

// Convert `doc` to a POJO, with virtuals attached
doc.toObject({ virtuals: true });

// Equivalent:
doc.toJSON({ virtuals: true });

上述 toJSON() 的警告还包括在 Mongoose 文档上调用 JSON.stringify() 的输出,因为 JSON.stringify() 调用 toJSON()。 要在 JSON.stringify() 输出中包含虚拟值,你可以在调用 JSON.stringify() 之前在文档上调用 toObject({ virtuals: true }),或者在结构上设置 toJSON: { virtuals: true } 选项。

// Explicitly add virtuals to `JSON.stringify()` output
JSON.stringify(doc.toObject({ virtuals: true }));

// Or, to automatically attach virtuals to `JSON.stringify()` output:
const personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
}, {
  toJSON: { virtuals: true } // <-- include virtuals in `JSON.stringify()`
});

你还可以向虚拟设备添加自定义设置器,以便你通过 fullName 虚拟设备设置名字和姓氏。

// Again that can be done either by adding it to schema options:
const personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
}, {
  virtuals: {
    fullName: {
      get() {
        return this.name.first + ' ' + this.name.last;
      },
      set(v) {
        this.name.first = v.substr(0, v.indexOf(' '));
        this.name.last = v.substr(v.indexOf(' ') + 1);
      }
    }
  }
});

// Or by using the virtual method as following:
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"

虚拟属性设置器在其他验证之前应用。 因此,即使需要 firstlast 名称字段,上面的示例仍然有效。

只有非虚拟属性才能作为查询和字段选择的一部分。 由于虚拟数据不存储在 MongoDB 中,因此你无法使用它们进行查询。

你可以 在这里了解有关虚拟的更多信息

别名

别名是一种特殊类型的虚拟,其中 getter 和 setter 无缝地获取和设置另一个属性。 这对于节省网络带宽很方便,因此你可以将数据库中存储的短属性名称转换为较长的名称,以提高代码可读性。

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

// Setting `name` will propagate to `n`
const 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' }

你还可以在嵌套路径上声明别名。 使用嵌套结构和 subdocuments 更容易,但你也可以内联声明嵌套路径别名,只要使用完整嵌套路径 nested.myProp 作为别名即可。

const childSchema = new Schema({
  n: {
    type: String,
    alias: 'name'
  }
}, { _id: false });

const parentSchema = new Schema({
  // If in a child schema, alias doesn't need to include the full nested path
  c: childSchema,
  name: {
    f: {
      type: String,
      // Alias needs to include the full nested path if declared inline
      alias: 'name.first'
    }
  }
});

选项

结构有一些可配置选项,可以传递给构造函数或 set 方法:

new Schema({ /* ... */ }, options);

// or

const schema = new Schema({ /* ... */ });
schema.set(option, value);

有效选项:

选项:自动索引

默认情况下,成功连接到 MongoDB 后,Mongoose 的 init() 功能 通过调用 Model.createIndexes() 创建模型结构中定义的所有索引。 自动创建索引非常适合开发和测试环境。 但索引构建也会给生产数据库带来巨大的负载。 如果你想在生产中仔细管理索引,可以将 autoIndex 设置为 false。

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

默认情况下,autoIndex 选项设置为 true。 你可以通过设置 mongoose.set('autoIndex', false); 更改此默认值

选项:自动创建

Mongoose 在构建索引之前,默认会调用 Model.createCollection() 在 MongoDB 中创建底层集合。 调用 createCollection() 会根据 排序规则选项 设置 集合的默认排序规则,如果设置了 capped 结构选项,则将该集合建立为上限集合。

你可以通过使用 mongoose.set('autoCreate', false)autoCreate 设置为 false 来禁用此行为。 与 autoIndex 一样,autoCreate 对于开发和测试环境很有帮助,但你可能希望在生产环境中禁用它,以避免不必要的数据库调用。

不幸的是,createCollection() 无法更改现有集合。 例如,如果你将 capped: { size: 1024 } 添加到结构中并且现有集合没有上限,则 createCollection() 将覆盖现有集合。 这是因为 MongoDB 服务器不允许在不先删除集合的情况下更改集合的选项。

const schema = new Schema({ name: String }, {
  autoCreate: false,
  capped: { size: 1024 }
});
const Test = mongoose.model('Test', schema);

// No-op if collection already exists, even if the collection is not capped.
// This means that `capped` won't be applied if the 'tests' collection already exists.
await Test.createCollection();

选项:缓冲区命令

默认情况下,当连接断开时,Mongoose 会缓冲命令,直到驱动程序设法重新连接。 要禁用缓冲,请将 bufferCommands 设置为 false。

const schema = new Schema({ /* ... */ }, { bufferCommands: false });

结构 bufferCommands 选项会覆盖全局 bufferCommands 选项。

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

选项:bufferTimeoutMS

如果 bufferCommands 打开,此选项设置 Mongoose 缓冲在抛出错误之前等待的最长时间。 如果未指定,Mongoose 将使用 10000(10 秒)。

// If an operation is buffered for more than 1 second, throw an error.
const schema = new Schema({ /* ... */ }, { bufferTimeoutMS: 1000 });

选项:上限

Mongoose 支持 MongoDB capped 集合。 要将底层 MongoDB 集合指定为 capped,请将 capped 选项设置为 bytes 中集合的最大大小。

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

如果你想传递 max 等其他选项,也可以将 capped 选项设置为对象。 在这种情况下,你必须显式传递 size 选项,这是必需的。

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

选项:集合

默认情况下,Mongoose 通过将模型名称传递给 utils.toCollectionName 方法来生成集合名称。 此方法使名称复数。 如果你需要为集合使用不同的名称,请设置此选项。

const dataSchema = new Schema({ /* ... */ }, { collection: 'data' });

选项:discriminatorKey

当你定义 discriminator 时,Mongoose 会向你的结构添加一个路径,该路径存储文档是哪个鉴别器的实例。 默认情况下,Mongoose 添加 __t 路径,但你可以设置 discriminatorKey 来覆盖此默认路径。

const baseSchema = new Schema({}, { discriminatorKey: 'type' });
const BaseModel = mongoose.model('Test', baseSchema);

const personSchema = new Schema({ name: String });
const PersonModel = BaseModel.discriminator('Person', personSchema);

const doc = new PersonModel({ name: 'James T. Kirk' });
// Without `discriminatorKey`, Mongoose would store the discriminator
// key in `__t` instead of `type`
doc.type; // 'Person'

选项:排除索引

excludeIndexestrue 时,Mongoose 将不会从给定的子文档结构创建索引。 此选项仅在子文档路径或文档数组路径中使用结构时才有效,如果在模型的顶层结构上设置,Mongoose 会忽略此选项。 默认为 false

const childSchema1 = Schema({
  name: { type: String, index: true }
});

const childSchema2 = Schema({
  name: { type: String, index: true }
}, { excludeIndexes: true });

// Mongoose will create an index on `child1.name`, but **not** `child2.name`, because `excludeIndexes`
// is true on `childSchema2`
const User = new Schema({
  name: { type: String, index: true },
  child1: childSchema1,
  child2: childSchema2
});

选项: ID

默认情况下,Mongoose 为每个结构分配一个 id 虚拟 getter,它返回文档的 _id 字段转换为字符串,或者在 ObjectIds 的情况下,返回其十六进制字符串。 如果你不希望将 id getter 添加到你的结构中,你可以通过在结构构建时传递此选项来禁用它。

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

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

选项:_id

如果未将某个字段传递给 结构 构造函数,Mongoose 默认会为每个结构分配一个 _id 字段。 分配的类型是 ObjectId,以与 MongoDB 的默认行为一致。 如果你根本不想将 _id 添加到你的结构中,你可以使用此选项禁用它。

你可以 only 在子文档上使用此选项。 Mongoose 无法在不知道 id 的情况下保存文档,因此如果你尝试保存没有 _id 的文档,将会收到错误消息。

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

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

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

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

选项:最小化

默认情况下,Mongoose 将通过删除空对象来建立 "minimize" 结构。

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

// will store `inventory` field if it is not empty
const frodo = new Character({ name: 'Frodo', inventory: { ringOfPower: 1 } });
await frodo.save();
let doc = await Character.findOne({ name: 'Frodo' }).lean();
doc.inventory; // { ringOfPower: 1 }

// will not store `inventory` field if it is empty
const sam = new Character({ name: 'Sam', inventory: {} });
await sam.save();
doc = await Character.findOne({ name: 'Sam' }).lean();
doc.inventory; // undefined

可以通过将 minimize 选项设置为 false 来覆盖此行为。 然后它将存储空对象。

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

// will store `inventory` if empty
const sam = new Character({ name: 'Sam', inventory: {} });
await sam.save();
doc = await Character.findOne({ name: 'Sam' }).lean();
doc.inventory; // {}

要检查对象是否为空,可以使用 $isEmpty() 辅助程序:

const sam = new Character({ name: 'Sam', inventory: {} });
sam.$isEmpty('inventory'); // true

sam.inventory.barrowBlade = 1;
sam.$isEmpty('inventory'); // false

选项:阅读

允许在结构级别设置 query#read 选项,为我们提供了一种将默认 ReadPreferences 应用于从模型派生的所有查询的方法。

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

每个首选项的别名也是允许的,因此我们可以简单地传递 'sp',而不必键入“secondaryPreferred”并导致拼写错误。

读取选项还允许我们指定标签集。 这些告诉 driver 它应该尝试从副本集的哪些成员中读取。 了解有关标签集 herehere 的更多信息。

注意: 你还可以在连接时指定驱动程序读取首选项 strategy 选项:

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

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

选项:写关注

允许在结构级别设置 写下关注

const schema = new Schema({ name: String }, {
  writeConcern: {
    w: 'majority',
    j: true,
    wtimeout: 1000
  }
});

选项:分片键

当我们有 分片 MongoDB 架构 时,使用 shardKey 选项。 每个分片集合都有一个分片键,该键必须存在于所有插入/更新操作中。 我们只需将此结构选项设置为相同的分片键即可完成。

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

请注意,Mongoose 不会为你发送 shardcollection 命令。 你必须自己配置你的分片。

选项:严格

strict 选项(默认启用)可确保传递给模型构造函数的、结构中未指定的值不会保存到数据库中。

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

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

这也会影响使用 doc.set() 设置属性值。

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

可以通过传递第二个布尔参数在模型实例级别覆盖该值:

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

strict 选项也可以设置为 "throw",这将导致产生错误而不是丢弃坏数据。

注意: 无论结构选项如何,结构中不存在的实例上设置的任何键/值都将始终被忽略。

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

选项:严格查询

Mongoose 支持单独的 strictQuery 选项以避免查询过滤器的严格结构。 这是因为空查询过滤器会导致 Mongoose 返回模型中的所有文档,这可能会导致问题。

const mySchema = new Schema({ field: Number }, { strict: true });
const MyModel = mongoose.model('Test', mySchema);
// Mongoose will filter out `notInSchema: 1` because `strict: true`, meaning this query will return
// _all_ documents in the 'tests' collection
MyModel.find({ notInSchema: 1 });

strict 选项确实适用于更新。 strictQuery 选项是查询过滤器的 just

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

Mongoose 有一个单独的 strictQuery 选项,可以将 filter 参数的严格结构切换为查询。

const mySchema = new Schema({ field: Number }, {
  strict: true,
  strictQuery: false // Turn off strict mode for query filters
});
const MyModel = mongoose.model('Test', mySchema);
// Mongoose will not strip out `notInSchema: 1` because `strictQuery` is false
MyModel.find({ notInSchema: 1 });

一般来说,我们确实建议将用户定义的对象作为查询过滤器传递:

// Don't do this!
const docs = await MyModel.find(req.query);

// Do this instead:
const docs = await MyModel.find({ name: req.query.name, age: req.query.age }).setOptions({ sanitizeFilter: true });

在 Mongoose 7 中,strictQuery 默认为 false。 但是,你可以全局覆盖此行为:

// Set `strictQuery` to `true` to omit unknown fields in queries.
mongoose.set('strictQuery', true);

选项:toJSON

toObject 选项完全相同,但仅在调用文档的 toJSON 方法 时适用。

const schema = new Schema({ name: String });
schema.path('name').get(function(v) {
  return v + ' is my name';
});
schema.set('toJSON', { getters: true, virtuals: false });
const M = mongoose.model('Person', schema);
const 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" }

要查看所有可用的 toJSON/toObject 选项,请阅读 this

选项:对象

文档有一个 toObject 方法,可将 mongoose 文档转换为纯 JavaScript 对象。 此方法接受一些选项。 我们可以在结构级别声明这些选项,并默认将它们应用于结构的所有文档,而不是在每个文档的基础上应用这些选项。

要让所有虚拟值显示在 console.log 输出中,请将 toObject 选项设置为 { getters: true }

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

要查看所有可用的 toObject 选项,请阅读 this

选项:类型键

默认情况下,如果你的结构中有一个带有键 'type' 的对象,mongoose 会将其解释为类型声明。

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

然而,对于像 geoJSON 这样的应用, 'type' 属性很重要。 如果要控制 mongoose 使用哪个键来查找类型声明,请设置 'typeKey' 结构选项。

const schema = new Schema({
  // Mongoose interprets 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

选项:保存前验证

默认情况下,文档在保存到数据库之前会自动验证。 这是为了防止保存无效文档。 如果你想手动处理验证,并且能够保存未通过验证的对象,你可以将 validateBeforeSave 设置为 false。

const schema = new Schema({ name: String });
schema.set('validateBeforeSave', false);
schema.path('name').validate(function(value) {
  return value != null;
});
const M = mongoose.model('Person', schema);
const 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

选项:版本密钥

versionKey 是 Mongoose 首次创建每个文档时设置的属性。 该键值包含文档的内部 revisionversionKey 选项是一个字符串,表示用于版本控制的路径。 默认为 __v。 如果这与你的应用冲突,你可以这样配置:

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

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

请注意,Mongoose 的默认版本控制是 not,这是一个完整的 乐观并发 解决方案。 Mongoose 的默认版本控制仅在数组上运行,如下所示。

// 2 copies of the same document
const doc1 = await Model.findOne({ _id });
const doc2 = await Model.findOne({ _id });

// Delete first 3 comments from `doc1`
doc1.comments.splice(0, 3);
await doc1.save();

// The below `save()` will throw a VersionError, because you're trying to
// modify the comment at index 1, and the above `splice()` removed that
// comment.
doc2.set('comments.1.body', 'new comment');
await doc2.save();

如果需要 save() 的乐观并发支持,可以设置 optimisticConcurrency 选项

还可以通过将 versionKey 设置为 false 来禁用文档版本控制。 不要禁用版本控制,除非你 知道你在做什么

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

Mongoose 仅在你使用 save() 时更新版本密钥。 如果使用 update()findOneAndUpdate() 等,Mongoose 会 not 更新版本密钥。 作为解决方法,你可以使用以下中间件。

schema.pre('findOneAndUpdate', function() {
  const update = this.getUpdate();
  if (update.__v != null) {
    delete update.__v;
  }
  const keys = ['$set', '$setOnInsert'];
  for (const key of keys) {
    if (update[key] != null && update[key].__v != null) {
      delete update[key].__v;
      if (Object.keys(update[key]).length === 0) {
        delete update[key];
      }
    }
  }
  update.$inc = update.$inc || {};
  update.$inc.__v = 1;
});

选项:乐观并发

乐观并发 是一种策略,可确保你正在更新的文档在使用 find()findOne() 加载文档和使用 save() 更新文档之间不会发生更改。

例如,假设你有一个包含 photos 列表的 House 模型,以及表示该房屋是否出现在搜索中的 status。 假设具有 'APPROVED' 状态的房屋必须至少有两个 photos。 你可以实现批准内部文档的逻辑,如下所示:

async function markApproved(id) {
  const house = await House.findOne({ _id });
  if (house.photos.length < 2) {
    throw new Error('House must have at least two photos!');
  }

  house.status = 'APPROVED';
  await house.save();
}

markApproved() 函数单独看起来是正确的,但可能存在一个潜在的问题: 如果另一个函数在 findOne() 调用和 save() 调用之间删除了房子的照片怎么办? 例如,下面的代码将会成功:

const house = await House.findOne({ _id });
if (house.photos.length < 2) {
  throw new Error('House must have at least two photos!');
}

const house2 = await House.findOne({ _id });
house2.photos = [];
await house2.save();

// Marks the house as 'APPROVED' even though it has 0 photos!
house.status = 'APPROVED';
await house.save();

如果你在 House 模型的结构上设置 optimisticConcurrency 选项,上述脚本将引发错误。

const House = mongoose.model('House', Schema({
  status: String,
  photos: [String]
}, { optimisticConcurrency: true }));

const house = await House.findOne({ _id });
if (house.photos.length < 2) {
  throw new Error('House must have at least two photos!');
}

const house2 = await House.findOne({ _id });
house2.photos = [];
await house2.save();

// Throws 'VersionError: No matching document found for id "..." version 0'
house.status = 'APPROVED';
await house.save();

选项:排序规则

为每个查询和聚合设置默认 collation这是适合初学者的排序规则概述

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

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

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

选项:时间序列

如果你在结构上设置 timeseries 选项,Mongoose 将为你从该结构创建的任何模型创建 时间序列集合

const schema = Schema({ name: String, timestamp: Date, metadata: Object }, {
  timeseries: {
    timeField: 'timestamp',
    metaField: 'metadata',
    granularity: 'hours'
  },
  autoCreate: false,
  expireAfterSeconds: 86400
});

// `Test` collection will be a timeseries collection
const Test = db.model('Test', schema);

选项:跳过版本控制

skipVersioning 允许从版本控制中排除路径(即,即使更新这些路径,内部修订也不会增加)。 除非你知道自己在做什么,否则请勿这样做。 对于子文档,请使用完全限定路径将其包含在父文档中。

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

选项:时间戳

timestamps 选项告诉 Mongoose 将 createdAtupdatedAt 字段分配给你的结构。 指定的类型是 日期

默认情况下,字段名称为 createdAtupdatedAt。 通过设置 timestamps.createdAttimestamps.updatedAt 自定义字段名称。

timestamps 的底层工作方式是:

  • 如果你创建一个新文档,mongoose 只需将 createdAtupdatedAt 设置为创建时间。
  • 如果更新文档,mongoose 会将 updatedAt 添加到 $set 对象。
  • 如果你在更新操作上设置 upsert: true,mongoose 将使用 $setOnInsert 运算符将 createdAt 添加到文档中,以防 upsert 操作导致新的插入文档。
const thingSchema = new Schema({ /* ... */ }, { timestamps: { createdAt: 'created_at' } });
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing();
await thing.save(); // `created_at` & `updatedAt` will be included

// With updates, Mongoose will add `updatedAt` to `$set`
await Thing.updateOne({}, { $set: { name: 'Test' } });

// If you set upsert: true, Mongoose will add `created_at` to `$setOnInsert` as well
await Thing.findOneAndUpdate({}, { $set: { name: 'Test2' } });

// Mongoose also adds timestamps to bulkWrite() operations
// See https://mongoose.nodejs.cn/docs/api/model.html#model_Model-bulkWrite
await Thing.bulkWrite([
  {
    insertOne: {
      document: {
        name: 'Jean-Luc Picard',
        ship: 'USS Stargazer'
      // Mongoose will add `created_at` and `updatedAt`
      }
    }
  },
  {
    updateOne: {
      filter: { name: 'Jean-Luc Picard' },
      update: {
        $set: {
          ship: 'USS Enterprise'
        // Mongoose will add `updatedAt`
        }
      }
    }
  }
]);

默认情况下,Mongoose 使用 new Date() 来获取当前时间。 如果你想覆盖 Mongoose 用来获取当前时间的函数,你可以设置 timestamps.currentTime 选项。 Mongoose 每当需要获取当前时间时就会调用 timestamps.currentTime 函数。

const schema = Schema({
  createdAt: Number,
  updatedAt: Number,
  name: String
}, {
  // Make Mongoose use Unix time (seconds since Jan 1, 1970)
  timestamps: { currentTime: () => Math.floor(Date.now() / 1000) }
});

选项:插件标签

Mongoose 支持定义全局插件,即适用于所有结构的插件。

// Add a `meta` property to all schemas
mongoose.plugin(function myPlugin(schema) {
  schema.add({ meta: {} });
});

有时,你可能只想将给定的插件应用于某些结构。 在这种情况下,你可以将 pluginTags 添加到结构中:

const schema1 = new Schema({
  name: String
}, { pluginTags: ['useMetaPlugin'] });

const schema2 = new Schema({
  name: String
});

如果你使用 tags 选项调用 plugin(),Mongoose 将仅将该插件应用于在 pluginTags 中具有匹配条目的结构。

// Add a `meta` property to all schemas
mongoose.plugin(function myPlugin(schema) {
  schema.add({ meta: {} });
}, { tags: ['useMetaPlugin'] });

选项:选择填充路径

默认情况下,Mongoose 会自动为你 select() 任何填充的路径,除非你明确排除它们。

const bookSchema = new Schema({
  title: 'String',
  author: { type: 'ObjectId', ref: 'Person' }
});
const Book = mongoose.model('Book', bookSchema);

// By default, Mongoose will add `author` to the below `select()`.
await Book.find().select('title').populate('author');

// In other words, the below query is equivalent to the above
await Book.find().select('title author').populate('author');

要选择不选择默认填充的字段,请在结构中将 selectPopulatedPaths 设置为 false

const bookSchema = new Schema({
  title: 'String',
  author: { type: 'ObjectId', ref: 'Person' }
}, { selectPopulatedPaths: false });
const Book = mongoose.model('Book', bookSchema);

// Because `selectPopulatedPaths` is false, the below doc will **not**
// contain an `author` property.
const doc = await Book.findOne().select('title').populate('author');

选项:storeSubdocValidationError

由于遗留原因,当单个嵌套结构的子路径中存在验证错误时,Mongoose 也会记录单个嵌套结构路径中也存在验证错误。 例如:

const childSchema = new Schema({ name: { type: String, required: true } });
const parentSchema = new Schema({ child: childSchema });

const Parent = mongoose.model('Parent', parentSchema);

// Will contain an error for both 'child.name' _and_ 'child'
new Parent({ child: {} }).validateSync().errors;

在子结构上设置 storeSubdocValidationErrorfalse 以使 Mongoose 只报告父错误。

const childSchema = new Schema({
  name: { type: String, required: true }
}, { storeSubdocValidationError: false }); // <-- set on the child schema
const parentSchema = new Schema({ child: childSchema });

const Parent = mongoose.model('Parent', parentSchema);

// Will only contain an error for 'child.name'
new Parent({ child: {} }).validateSync().errors;

选项:集合选项

collationcapped 等选项会影响 Mongoose 在创建新集合时传递给 MongoDB 的选项。 Mongoose 结构支持大多数 MongoDB createCollection() 选项,但不是全部。 你可以使用 collectionOptions 选项来设置任何 createCollection() 选项; 当为你的结构调用 createCollection() 时,Mongoose 将使用 collectionOptions 作为默认值。

const schema = new Schema({ name: String }, {
  autoCreate: false,
  collectionOptions: {
    capped: true,
    max: 1000
  }
});
const Test = mongoose.model('Test', schema);

// Equivalent to `createCollection({ capped: true, max: 1000 })`
await Test.createCollection();

使用 ES6 类

结构有一个 loadClass() 方法,你可以使用它从 ES6 级 创建 Mongoose 结构:

下面是使用 loadClass() 从 ES6 类创建结构的示例:

class MyClass {
  myMethod() { return 42; }
  static myStatic() { return 42; }
  get myVirtual() { return 42; }
}

const schema = new mongoose.Schema();
schema.loadClass(MyClass);

console.log(schema.methods); // { myMethod: [Function: myMethod] }
console.log(schema.statics); // { myStatic: [Function: myStatic] }
console.log(schema.virtuals); // { myVirtual: VirtualType { ... } }

插件化

结构也是 pluggable,它允许我们将可重用的功能打包到插件中,这些插件可以与社区或仅在你的项目之间共享。

进一步阅读

这是 Mongoose 结构的替代介绍

为了充分利用 MongoDB,你需要学习 MongoDB 结构设计的基础知识。 SQL 结构设计(第三范式)是针对 最小化存储成本 设计的,而 MongoDB 结构设计是为了尽可能快地进行常见查询。 MongoDB 结构设计博客系列的 6 个经验法则 是学习快速查询基本规则的绝佳资源。

希望掌握 Node.js 中 MongoDB 结构设计的用户应该查看 MongoDB Node.js 驱动程序 的原作者 Christian Kvalheim 的 MongoDB 结构设计小书。 本书向你展示如何为一系列用例(包括电子商务、维基和预约)实现高性能结构。

下一步

现在我们已经介绍了 Schemas,让我们来看看 SchemaTypes