綱要 (Schemas)

如果您還沒這麼做,請花一點時間閱讀快速入門,以了解 Mongoose 的運作方式。如果您要從 7.x 遷移到 8.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 中的每個鍵都定義了我們文件中會轉換為其關聯綱要類型的屬性。例如,我們定義了一個屬性 title,它將轉換為 String 綱要類型,以及一個屬性 date,它將轉換為 Date 綱要類型。

請注意,如果屬性只需要一個類型,可以使用簡寫表示法來指定(比較上面的 title 屬性與 date 屬性)。

鍵也可以分配包含更多鍵/類型定義的巢狀物件,例如上面的 meta 屬性。只要鍵的值是不具有 type 屬性的 POJO,就會發生這種情況。

在這些情況下,Mongoose 僅針對樹狀結構中的葉子建立實際的綱要路徑。(如上面的 meta.votesmeta.favs),而分支沒有實際的路徑。這樣做的副作用是,上面的 meta 不能有自己的驗證。如果需要在樹狀結構中進行驗證,則需要在樹狀結構中建立路徑 - 請參閱 子文件章節,以了解有關如何執行此操作的更多資訊。另請閱讀綱要類型指南的 混合子章節,以了解一些注意事項。

允許的綱要類型為

請在此處閱讀更多關於綱要類型的資訊。

綱要不僅定義了文件的結構和屬性的轉換,它們還定義了文件實例方法靜態模型方法複合索引和稱為中介軟體的文件生命週期掛鉤。

建立模型

若要使用我們的綱要定義,我們需要將 blogSchema 轉換為可以使用的模型。為此,我們將其傳遞到 mongoose.model(modelName, schema)

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

Id

預設情況下,Mongoose 會將 _id 屬性新增至您的綱要。

const schema = new Schema();

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

當您使用自動新增的 _id 屬性建立新文件時,Mongoose 會為您的文件建立一個新的_id,類型為 ObjectId

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 // <-- overwrite Mongoose's default `_id`
});
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

Mongoose 還會將 _id 屬性新增至子文件。您可以按照以下方式停用子文件上的 _id 屬性。Mongoose 允許儲存沒有 _id 屬性的子文件。

const nestedSchema = new Schema(
  { name: String },
  { _id: false } // <-- disable `_id`
);
const schema = new Schema({
  subdoc: nestedSchema,
  docArray: [nestedSchema]
});
const Test = mongoose.model('Test', schema);

// Neither `subdoc` nor `docArray.0` will have an `_id`
await Test.create({
  subdoc: { name: 'test 1' },
  docArray: [{ name: 'test 2' }]
});

或者,您可以使用以下語法停用 _id

const nestedSchema = new Schema({
  _id: false, // <-- disable _id
  name: String
});

實例方法

Models 的實例是文件。文件有許多自己的內建實例方法。我們也可以定義自己的自訂文件實例方法。

// 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 文件方法可能會導致不可預測的結果。請參閱此處以瞭解更多詳細資訊。
  • 上面的範例直接使用 Schema.methods 物件來儲存實例方法。您也可以使用 此處所述的 Schema.method() 輔助函式。
  • 使用 ES6 箭頭函式 (=>) 宣告方法。箭頭函式明確阻止綁定 this,因此您的方法將無法存取文件,並且上面的範例將無法運作。

靜態方法

您也可以將靜態函式新增至模型。有三種等效的方法可以新增靜態函式

  • 將函式屬性新增至綱要建構函式的第二個引數 (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'));

使用 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 中定義這些索引, 路徑 層級綱要 層級。當建立複合索引時,必須在綱要層級定義索引。

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

但是每次都串連名字和姓氏可能會很麻煩。如果您想要對姓名進行一些額外的處理,例如移除變音符號呢?虛擬屬性 getter 可讓您定義不會持久儲存到 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()`
});

您也可以將自訂 setter 新增至虛擬屬性,這可讓您透過 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"

虛擬屬性 setter 會在其他驗證之前套用。因此,即使 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' }

您也可以在巢狀路徑上宣告別名。使用巢狀綱要和子文件更容易,但您也可以內聯宣告巢狀路徑別名,只要您使用完整的巢狀路徑 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);

有效的選項

選項:autoIndex

預設情況下,Mongoose 的 init() 函式 會在您成功連線到 MongoDB 後,呼叫 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); 來變更此預設值

選項:autoCreate

在 Mongoose 建立索引之前,它預設會呼叫 Model.createCollection(),以在 MongoDB 中建立基礎集合。呼叫 createCollection() 會根據排序規則選項設定集合的預設排序規則,並在您設定 capped 綱要選項時,將集合建立為固定大小集合。

您可以使用 mongoose.set('autoCreate', false),將 autoCreate 設定為 false 來停用此行為。與 autoIndex 類似,autoCreate 對於開發和測試環境很有幫助,但您可能想要在生產環境中停用它,以避免不必要的資料庫呼叫。

很遺憾,createCollection() 無法變更現有的集合。舉例來說,如果您在您的 schema 中加入 capped: { size: 1024 },而現有的集合並非 capped 集合,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();

選項:bufferCommands

預設情況下,當連線中斷時,Mongoose 會緩衝命令,直到驅動程式成功重新連線。若要停用緩衝,請將 bufferCommands 設為 false。

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

schema 的 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 });

選項:capped

Mongoose 支援 MongoDB 的 capped 集合。若要指定底層的 MongoDB 集合為 capped,請將 capped 選項設定為集合的最大大小(以 位元組 為單位)。

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

如果您想傳遞其他選項,例如 maxcapped 選項也可以設定為物件。在這種情況下,您必須明確傳遞必要的 size 選項。

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

選項:collection

Mongoose 預設會將 model 名稱傳遞給 utils.toCollectionName 方法來產生集合名稱。此方法會將名稱複數化。如果您需要不同的集合名稱,請設定此選項。

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

選項:discriminatorKey

當您定義一個 discriminator 時,Mongoose 會在您的 schema 中加入一個路徑,用於儲存文件是哪個 discriminator 的實例。預設情況下,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'

選項:excludeIndexes

excludeIndexestrue 時,Mongoose 不會從給定的子文件 schema 建立索引。此選項僅在 schema 用於子文件路徑或文件陣列路徑時才有效,如果設定在 model 的頂層 schema 上,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 預設會為您的每個 schema 指派一個 id 虛擬 getter,它會傳回文件的 _id 欄位,並轉換為字串,或是,在 ObjectId 的情況下,傳回其 hexString。如果您不希望將 id getter 加入您的 schema,您可以在建立 schema 時傳遞此選項來停用它。

// 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

如果沒有將 _id 傳遞給 Schema 建構子,Mongoose 預設會為您的每個 schema 指派一個 _id 欄位。指派的類型為 ObjectId,與 MongoDB 的預設行為一致。如果您完全不希望在您的 schema 中加入 _id,您可以使用此選項來停用它。

只能在子文件上使用此選項。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
});

選項:minimize

預設情況下,Mongoose 會透過移除空物件來「最小化」schema。

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

選項:read

允許在 schema 層級設定 query#read 選項,為我們提供將預設 ReadPreferences 套用至從 model 衍生的所有查詢的方法。

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' 並擔心拼字錯誤。

read 選項也允許我們指定標籤集。這些會告訴 驅動程式 應從哪個複本集的成員嘗試讀取。請在 這裡這裡 閱讀更多關於標籤集的資訊。

注意:您也可以在連線時指定驅動程式讀取偏好 策略 選項

// 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);

選項:writeConcern

允許在 schema 層級設定 write concern

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

選項:shardKey

當我們有一個 分片的 MongoDB 架構 時,會使用 shardKey 選項。每個分片的集合都會被給予一個分片鍵,該分片鍵必須出現在所有 insert/update 操作中。我們只需要將此 schema 選項設定為相同的分片鍵即可。

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

請注意,Mongoose 不會為您傳送 shardcollection 命令。您必須自行設定分片。

選項:strict

strict 選項(預設啟用)可確保傳遞給我們 model 建構子的值(未在我們的 schema 中指定)不會被儲存到資料庫中。

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

此值可以透過傳遞第二個布林引數在 model 實例層級被覆寫

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",這會導致產生錯誤,而不是捨棄錯誤的資料。

注意:在實例上設定的任何在您的 schema 中不存在的鍵/值都會被忽略,無論 schema 選項為何。

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

選項:strictQuery

Mongoose 支援單獨的 strictQuery 選項,以避免對查詢篩選器使用嚴格模式。這是因為空的查詢篩選器會導致 Mongoose 傳回 model 中的所有文件,這可能會導致問題。

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 選項適用於查詢篩選器。

// 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 中,預設情況下 strictQueryfalse。但是,您可以全域覆寫此行為

// 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 選項,請閱讀 此處

選項:toObject

文件有一個 toObject 方法,可將 mongoose 文件轉換為純 JavaScript 物件。此方法接受一些選項。我們可以不在每個文件的基礎上套用這些選項,而是在 schema 層級宣告這些選項,並讓它們預設套用至 schema 的所有文件。

若要讓所有虛擬屬性顯示在您的 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 選項,請閱讀 此處

選項:typeKey

預設情況下,如果您的 schema 中有一個帶有鍵 'type' 的物件,mongoose 會將其解讀為類型宣告。

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

但是,對於像 geoJSON 這樣的應用程式,'type' 屬性很重要。如果您想控制 mongoose 用哪個鍵來尋找類型宣告,請設定 'typeKey' schema 選項。

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

預設情況下,文件會在儲存到資料庫之前自動驗證。這是為了防止儲存無效的文件。如果您想手動處理驗證,並能夠儲存未通過驗證的物件,您可以將 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

versionKey 是 Mongoose 首次建立時在每個文件上設定的屬性。此鍵的值包含文件的內部 修訂版本versionKey 選項是一個字串,表示用於版本控制的路徑。預設值為 __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 的預設版本控制不是完整的 樂觀並行控制 解決方案。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' }

只有在您使用 save() 時,Mongoose 才會更新版本鍵。如果您使用 update()findOneAndUpdate() 等,Mongoose 將不會更新版本鍵。作為一種解決方案,您可以使用下面的 middleware。

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;
});

選項:optimisticConcurrency

樂觀並行控制是一種策略,可確保您正在更新的文件在您使用 find()findOne() 載入它時,以及在您使用 save() 更新它時之間沒有發生變更。

例如,假設您有一個 House model,其中包含 photos 的列表,以及一個表示此房屋是否顯示在搜尋中的 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 model 的 schema 上設定 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

為每個查詢和聚合設定預設的 collation這裡有一個關於 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

如果您在 schema 上設定 timeseries 選項,Mongoose 將會為您從該 schema 建立的任何 model 建立一個 時間序列集合

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

skipVersioning 允許從版本控制中排除路徑(即,即使更新了這些路徑,內部的修訂版本也不會遞增)。除非您知道自己在做什麼,否則請勿這樣做。對於子文件,請使用完全限定的路徑將其包含在父文件中。

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

選項:timestamps

timestamps 選項會告知 Mongoose 將 createdAtupdatedAt 欄位指派給您的 schema。指派的類型為 Date

預設情況下,欄位的名稱為 createdAtupdatedAt。透過設定 timestamps.createdAttimestamps.updatedAt 來客製化欄位名稱。

timestamps 的底層運作方式是

  • 如果您建立新文件,mongoose 只會將 createdAtupdatedAt 設定為建立時間。
  • 如果您更新文件,mongoose 會將 updatedAt 加入 $set 物件。
  • 如果您在更新操作中設定 upsert: true,Mongoose 會使用 $setOnInsert 運算子,以便在 upsert 操作導致插入新文件時,將 createdAt 新增至該文件。
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.dev.org.tw/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) }
});

選項:pluginTags

Mongoose 支援定義全域外掛程式,這些外掛程式會套用到所有 Schema。

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

有時,您可能只想將某個外掛程式套用到某些 Schema。在這種情況下,您可以將 pluginTags 新增至 Schema

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

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

如果您使用 tags 選項呼叫 plugin(),Mongoose 將只會將該外掛程式套用到 pluginTags 中具有相符條目的 Schema。

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

選項:selectPopulatedPaths

預設情況下,除非您明確排除,否則 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');

若要選擇不預設選取已填充的欄位,請在您的 Schema 中將 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

由於歷史因素,當單一巢狀 Schema 的子路徑中出現驗證錯誤時,Mongoose 也會記錄單一巢狀 Schema 路徑中出現驗證錯誤。例如

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;

將子 Schema 上的 storeSubdocValidationError 設定為 false,使 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;

選項:collectionOptions

諸如 collationcapped 之類的選項會影響 Mongoose 在建立新集合時傳遞給 MongoDB 的選項。Mongoose Schema 支援大多數 MongoDB createCollection() 選項,但並非全部。您可以使用 collectionOptions 選項來設定任何 createCollection() 選項;當 Mongoose 為您的 Schema 呼叫 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();

選項:autoSearchIndex

autoIndex 類似,只是會自動建立 Schema 中定義的任何 Atlas 搜尋索引。與 autoIndex 不同,此選項預設為 false。

const schema = new Schema({ name: String }, { autoSearchIndex: true });
schema.searchIndex({
  name: 'my-index',
  definition: { mappings: { dynamic: true } }
});
// Will automatically attempt to create the `my-index` search index.
const Test = mongoose.model('Test', schema);

選項:readConcern

讀取關注類似於 writeConcern,但是用於諸如 find()findOne() 之類的讀取操作。若要設定預設的 readConcern,請將 readConcern 選項傳遞給 Schema 建構函式,如下所示。

const eventSchema = new mongoose.Schema(
  { name: String },
  {
    readConcern: { level: 'available' } // <-- set default readConcern for all queries
  }
);

使用 ES6 類別

Schema 具有 loadClass() 方法,您可以使用它從 ES6 類別建立 Mongoose Schema

以下是使用 loadClass() 從 ES6 類別建立 Schema 的範例

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 { ... } }

可外掛

Schema 也是 可外掛的,這讓我們可以將可重複使用的功能封裝到外掛程式中,這些外掛程式可以與社群分享,或僅在您的專案之間分享。

延伸閱讀

以下是 Mongoose Schema 的另一種簡介

若要充分利用 MongoDB,您需要學習 MongoDB Schema 設計的基本知識。SQL Schema 設計(第三範式)旨在最小化儲存成本,而 MongoDB Schema 設計旨在盡可能加快常見查詢的速度。MongoDB Schema 設計的 6 大經驗法則部落格系列是學習如何加快查詢速度的基本規則的絕佳資源。

希望精通 Node.js 中 MongoDB Schema 設計的使用者應研究 《MongoDB Schema 設計小書》,作者是 MongoDB Node.js 驅動程式的原始作者 Christian Kvalheim。這本書向您展示了如何針對各種使用案例(包括電子商務、Wiki 和預約預訂)實作效能良好的 Schema。

接下來

既然我們已經介紹了 Schemas,讓我們來看看 SchemaTypes