SchemaTypes (綱要類型)

SchemaTypes 處理路徑的 預設值驗證gettersetter欄位選擇預設值,用於 查詢,以及 Mongoose 文件屬性的其他一般特性。

什麼是 SchemaType?

您可以將 Mongoose 綱要視為 Mongoose 模型的設定物件。然後,SchemaType 是個別屬性的設定物件。SchemaType 說明給定路徑應具有的類型、是否具有任何 getter/setter,以及該路徑的有效值。

const schema = new Schema({ name: String });
schema.path('name') instanceof mongoose.SchemaType; // true
schema.path('name') instanceof mongoose.Schema.Types.String; // true
schema.path('name').instance; // 'String'

SchemaType 與類型不同。換句話說,mongoose.ObjectId !== mongoose.Types.ObjectId。SchemaType 只是 Mongoose 的設定物件。mongoose.ObjectId SchemaType 的實例實際上不會建立 MongoDB ObjectIds,它只是綱要中路徑的設定。

以下是 Mongoose 中所有有效的 SchemaTypes。Mongoose 外掛程式也可以新增自訂 SchemaTypes,例如 int32。請查看 Mongoose 的外掛程式搜尋以尋找外掛程式。

範例

const schema = new Schema({
  name: String,
  binary: Buffer,
  living: Boolean,
  updated: { type: Date, default: Date.now },
  age: { type: Number, min: 18, max: 65 },
  mixed: Schema.Types.Mixed,
  _someId: Schema.Types.ObjectId,
  decimal: Schema.Types.Decimal128,
  array: [],
  ofString: [String],
  ofNumber: [Number],
  ofDates: [Date],
  ofBuffer: [Buffer],
  ofBoolean: [Boolean],
  ofMixed: [Schema.Types.Mixed],
  ofObjectId: [Schema.Types.ObjectId],
  ofArrays: [[]],
  ofArrayOfNumbers: [[Number]],
  nested: {
    stuff: { type: String, lowercase: true, trim: true }
  },
  map: Map,
  mapOfString: {
    type: Map,
    of: String
  }
});

// example use

const Thing = mongoose.model('Thing', schema);

const m = new Thing;
m.name = 'Statue of Liberty';
m.age = 125;
m.updated = new Date;
m.binary = Buffer.alloc(0);
m.living = false;
m.mixed = { any: { thing: 'i want' } };
m.markModified('mixed');
m._someId = new mongoose.Types.ObjectId;
m.array.push(1);
m.ofString.push('strings!');
m.ofNumber.unshift(1, 2, 3, 4);
m.ofDates.addToSet(new Date);
m.ofBuffer.pop();
m.ofMixed = [1, [], 'three', { four: 5 }];
m.nested.stuff = 'good';
m.map = new Map([['key', 'value']]);
m.save(callback);

type

type 是 Mongoose 綱要中的特殊屬性。當 Mongoose 在您的綱要中找到名為 type 的巢狀屬性時,Mongoose 會假設需要使用給定的類型定義 SchemaType。

// 3 string SchemaTypes: 'name', 'nested.firstName', 'nested.lastName'
const schema = new Schema({
  name: { type: String },
  nested: {
    firstName: { type: String },
    lastName: { type: String }
  }
});

因此,您需要額外的工作來定義綱要中名為 type 的屬性。例如,假設您正在建構一個股票投資組合應用程式,並且想要儲存資產的 type (股票、債券、ETF 等)。您可能會天真地將綱要定義如下所示

const holdingSchema = new Schema({
  // You might expect `asset` to be an object that has 2 properties,
  // but unfortunately `type` is special in Mongoose so mongoose
  // interprets this schema to mean that `asset` is a string
  asset: {
    type: String,
    ticker: String
  }
});

但是,當 Mongoose 看到 type: String 時,它會假設您的意思是 asset 應該是字串,而不是具有屬性 type 的物件。定義具有屬性 type 的物件的正確方法如下所示。

const holdingSchema = new Schema({
  asset: {
    // Workaround to make sure Mongoose knows `asset` is an object
    // and `asset.type` is a string, rather than thinking `asset`
    // is a string.
    type: { type: String },
    ticker: String
  }
});

SchemaType 選項

您可以使用類型直接宣告綱要類型,或使用具有 type 屬性的物件。

const schema1 = new Schema({
  test: String // `test` is a path of type String
});

const schema2 = new Schema({
  // The `test` object contains the "SchemaType options"
  test: { type: String } // `test` is a path of type string
});

除了 type 屬性之外,您還可以為路徑指定其他屬性。例如,如果您想在儲存之前將字串轉換為小寫

const schema2 = new Schema({
  test: {
    type: String,
    lowercase: true // Always convert `test` to lowercase
  }
});

您可以將任何屬性新增至 SchemaType 選項。許多外掛程式都依賴自訂 SchemaType 選項。例如,如果您在 SchemaType 選項中設定 autopopulate: truemongoose-autopopulate 外掛程式會自動填充路徑。Mongoose 支援多個內建的 SchemaType 選項,例如上述範例中的 lowercase

lowercase 選項僅適用於字串。有些選項適用於所有綱要類型,有些則適用於特定綱要類型。

所有綱要類型

  • required:布林值或函數,如果為 true,則會為此屬性新增必填驗證器
  • default:任何或函數,設定路徑的預設值。如果值為函數,則函數的傳回值將用作預設值。
  • select:布林值,指定查詢的預設投影
  • validate:函數,為此屬性新增驗證器函數
  • get:函數,使用 Object.defineProperty() 為此屬性定義自訂 getter。
  • set:函數,使用 Object.defineProperty() 為此屬性定義自訂 setter。
  • alias:字串,僅限 mongoose >= 4.10.0。定義具有指定名稱的虛擬,該虛擬會取得/設定此路徑。
  • immutable:布林值,將路徑定義為不可變。除非父文件具有 isNew: true,否則 Mongoose 會阻止您變更不可變的路徑。
  • transform:函數,當您呼叫 Document#toJSON() 函數時,Mongoose 會呼叫此函數,包括當您JSON.stringify() 文件時。
const numberSchema = new Schema({
  integerOnly: {
    type: Number,
    get: v => Math.round(v),
    set: v => Math.round(v),
    alias: 'i'
  }
});

const Number = mongoose.model('Number', numberSchema);

const doc = new Number();
doc.integerOnly = 2.001;
doc.integerOnly; // 2
doc.i; // 2
doc.i = 3.001;
doc.integerOnly; // 3
doc.i; // 3

索引

您也可以使用綱要類型選項定義 MongoDB 索引

  • index:布林值,是否在此屬性上定義索引
  • unique:布林值,是否在此屬性上定義唯一索引
  • sparse:布林值,是否在此屬性上定義稀疏索引
const schema2 = new Schema({
  test: {
    type: String,
    index: true,
    unique: true // Unique index. If you specify `unique: true`
    // specifying `index: true` is optional if you do `unique: true`
  }
});

字串

  • lowercase:布林值,是否始終在值上呼叫 .toLowerCase()
  • uppercase:布林值,是否始終在值上呼叫 .toUpperCase()
  • trim:布林值,是否始終在值上呼叫 .trim()
  • match:RegExp,建立一個驗證器,檢查值是否符合給定的正規表示式
  • enum:陣列,建立一個驗證器,檢查值是否在給定的陣列中。
  • minLength:數字,建立一個驗證器,檢查值的長度是否不小於給定的數字
  • maxLength:數字,建立一個驗證器,檢查值的長度是否不大於給定的數字
  • populate:物件,設定預設填充選項

數字

  • min:數字,建立一個驗證器,檢查值是否大於或等於給定的最小值。
  • max:數字,建立一個驗證器,檢查值是否小於或等於給定的最大值。
  • enum:陣列,建立一個驗證器,檢查值是否嚴格等於給定陣列中的其中一個值。
  • populate:物件,設定預設填充選項

日期

  • min:日期,建立一個驗證器,檢查值是否大於或等於給定的最小值。
  • max:日期,建立一個驗證器,檢查值是否小於或等於給定的最大值。
  • expires:數字或字串,建立 TTL 索引,其值以秒表示。

ObjectId

使用注意事項

字串

若要將路徑宣告為字串,您可以使用全域建構函式 String 或字串 'String'

const schema1 = new Schema({ name: String }); // name will be cast to string
const schema2 = new Schema({ name: 'String' }); // Equivalent

const Person = mongoose.model('Person', schema2);

如果您傳遞具有 toString() 函數的元素,則 Mongoose 會呼叫它,除非該元素是陣列,或者 toString() 函數嚴格等於 Object.prototype.toString()

new Person({ name: 42 }).name; // "42" as a string
new Person({ name: { toString: () => 42 } }).name; // "42" as a string

// "undefined", will get a cast error if you `save()` this document
new Person({ name: { foo: 42 } }).name;

數字

若要將路徑宣告為數字,您可以使用全域建構函式 Number 或字串 'Number'

const schema1 = new Schema({ age: Number }); // age will be cast to a Number
const schema2 = new Schema({ age: 'Number' }); // Equivalent

const Car = mongoose.model('Car', schema2);

有多種類型的值會成功轉換為數字。

new Car({ age: '15' }).age; // 15 as a Number
new Car({ age: true }).age; // 1 as a Number
new Car({ age: false }).age; // 0 as a Number
new Car({ age: { valueOf: () => 83 } }).age; // 83 as a Number

如果您傳遞一個具有傳回數字的 valueOf() 函數的物件,則 Mongoose 會呼叫它並將傳回的值指派給路徑。

不會轉換值 nullundefined

NaN、轉換為 NaN 的字串、陣列以及沒有 valueOf() 函數的物件都會在驗證後產生CastError,這表示它不會在初始化時擲回,而只會在驗證時擲回。

日期

內建的 Date 方法 連線到 mongoose 變更追蹤邏輯,這表示如果您在文件中使用 Date 並使用 setMonth() 之類的方法修改它,mongoose 將不會知道此變更,且 doc.save() 將不會保存此修改。如果您必須使用內建方法修改 Date 類型,請在儲存之前使用 doc.markModified('pathToYourDate') 通知 mongoose 該變更。

const Assignment = mongoose.model('Assignment', { dueDate: Date });
const doc = await Assignment.findOne();
doc.dueDate.setMonth(3);
await doc.save(); // THIS DOES NOT SAVE YOUR CHANGE

doc.markModified('dueDate');
await doc.save(); // works

緩衝區

若要將路徑宣告為緩衝區,您可以使用全域建構函式 Buffer 或字串 'Buffer'

const schema1 = new Schema({ binData: Buffer }); // binData will be cast to a Buffer
const schema2 = new Schema({ binData: 'Buffer' }); // Equivalent

const Data = mongoose.model('Data', schema2);

Mongoose 會成功將以下值轉換為緩衝區。

const file1 = new Data({ binData: 'test'}); // {"type":"Buffer","data":[116,101,115,116]}
const file2 = new Data({ binData: 72987 }); // {"type":"Buffer","data":[27]}
const file4 = new Data({ binData: { type: 'Buffer', data: [1, 2, 3]}}); // {"type":"Buffer","data":[1,2,3]}

混合

「任何皆可」的 SchemaType。Mongoose 不會對混合路徑執行任何轉換。您可以使用 Schema.Types.Mixed 或傳遞空物件常值來定義混合路徑。以下是等效的。

const Any = new Schema({ any: {} });
const Any = new Schema({ any: Object });
const Any = new Schema({ any: Schema.Types.Mixed });
const Any = new Schema({ any: mongoose.Mixed });

由於 Mixed 是無綱要類型,因此您可以將值變更為您喜歡的任何其他值,但是 Mongoose 會失去自動偵測和儲存這些變更的能力。若要告知 Mongoose Mixed 類型的值已變更,您需要呼叫 doc.markModified(path),傳遞您剛變更之 Mixed 類型的路徑。

為了避免這些副作用,可以使用子文件路徑來代替。

person.anything = { x: [3, 4, { y: 'changed' }] };
person.markModified('anything');
person.save(); // Mongoose will save changes to `anything`.

ObjectIds

ObjectId 是一種特殊類型,通常用於唯一識別碼。以下是如何使用路徑 driver (屬於 ObjectId) 宣告綱要的方法

const mongoose = require('mongoose');
const carSchema = new mongoose.Schema({ driver: mongoose.ObjectId });

ObjectId 是一個類別,而 ObjectIds 是物件。然而,它們通常以字串形式表示。當您使用 toString() 將 ObjectId 轉換為字串時,您會得到一個 24 個字元的十六進位字串。

const Car = mongoose.model('Car', carSchema);

const car = new Car();
car.driver = new mongoose.Types.ObjectId();

typeof car.driver; // 'object'
car.driver instanceof mongoose.Types.ObjectId; // true

car.driver.toString(); // Something like "5e1a0651741b255ddda996c4"

布林值

在 Mongoose 中,布林值是純 JavaScript 布林值。預設情況下,Mongoose 會將以下值轉換為 true

  • true
  • 'true'
  • 1
  • '1'
  • 'yes'

Mongoose 會將以下值轉換為 false

  • false
  • 'false'
  • 0
  • '0'
  • 'no'

任何其他值都會導致 CastError。您可以使用 convertToTrueconvertToFalse 屬性來修改 Mongoose 轉換為 true 或 false 的值,這些屬性是 JavaScript sets

const M = mongoose.model('Test', new Schema({ b: Boolean }));
console.log(new M({ b: 'nay' }).b); // undefined

// Set { false, 'false', 0, '0', 'no' }
console.log(mongoose.Schema.Types.Boolean.convertToFalse);

mongoose.Schema.Types.Boolean.convertToFalse.add('nay');
console.log(new M({ b: 'nay' }).b); // false

陣列

Mongoose 支援 SchemaTypes 的陣列和 子文件的陣列。SchemaTypes 的陣列也稱為原始陣列,而子文件的陣列也稱為文件陣列

const ToySchema = new Schema({ name: String });
const ToyBoxSchema = new Schema({
  toys: [ToySchema],
  buffers: [Buffer],
  strings: [String],
  numbers: [Number]
  // ... etc
});

陣列很特別,因為它們隱式具有預設值 [] (空陣列)。

const ToyBox = mongoose.model('ToyBox', ToyBoxSchema);
console.log((new ToyBox()).toys); // []

若要覆寫此預設值,您需要將預設值設定為 undefined

const ToyBoxSchema = new Schema({
  toys: {
    type: [ToySchema],
    default: undefined
  }
});

注意:指定一個空陣列等同於 Mixed。以下所有方式都會建立 Mixed 的陣列

const Empty1 = new Schema({ any: [] });
const Empty2 = new Schema({ any: Array });
const Empty3 = new Schema({ any: [Schema.Types.Mixed] });
const Empty4 = new Schema({ any: [{}] });

映射

MongooseMapJavaScript 的 Map 類別的子類別。在這些文件中,我們將交替使用「映射」和 MongooseMap 這兩個詞。在 Mongoose 中,映射是您如何建立具有任意鍵的巢狀文件。

注意:在 Mongoose 映射中,為了將文件儲存在 MongoDB 中,鍵必須是字串。

const userSchema = new Schema({
  // `socialMediaHandles` is a map whose values are strings. A map's
  // keys are always strings. You specify the type of values using `of`.
  socialMediaHandles: {
    type: Map,
    of: String
  }
});

const User = mongoose.model('User', userSchema);
// Map { 'github' => 'vkarpov15', 'twitter' => '@code_barbarian' }
console.log(new User({
  socialMediaHandles: {
    github: 'vkarpov15',
    twitter: '@code_barbarian'
  }
}).socialMediaHandles);

上面的範例並未明確將 githubtwitter 宣告為路徑,但是,由於 socialMediaHandles 是一個映射,因此您可以儲存任意的鍵/值對。然而,由於 socialMediaHandles 是一個映射,您必須使用 .get() 來取得鍵的值,並使用 .set() 來設定鍵的值。

const user = new User({
  socialMediaHandles: {}
});

// Good
user.socialMediaHandles.set('github', 'vkarpov15');
// Works too
user.set('socialMediaHandles.twitter', '@code_barbarian');
// Bad, the `myspace` property will **not** get saved
user.socialMediaHandles.myspace = 'fail';

// 'vkarpov15'
console.log(user.socialMediaHandles.get('github'));
// '@code_barbarian'
console.log(user.get('socialMediaHandles.twitter'));
// undefined
user.socialMediaHandles.github;

// Will only save the 'github' and 'twitter' properties
user.save();

映射類型在 MongoDB 中以 BSON 物件儲存。BSON 物件中的鍵是有序的,因此這表示會保留映射的 插入順序屬性。

Mongoose 支援特殊的 $* 語法來 擴展映射中的所有元素。例如,假設您的 socialMediaHandles 映射包含一個 ref

const userSchema = new Schema({
  socialMediaHandles: {
    type: Map,
    of: new Schema({
      handle: String,
      oauth: {
        type: ObjectId,
        ref: 'OAuth'
      }
    })
  }
});
const User = mongoose.model('User', userSchema);

若要擴展每個 socialMediaHandles 條目的 oauth 屬性,您應該在 socialMediaHandles.$*.oauth 上進行擴展

const user = await User.findOne().populate('socialMediaHandles.$*.oauth');

UUID

Mongoose 也支援 UUID 類型,該類型將 UUID 實例儲存為 Node.js 緩衝區。我們建議在 Mongoose 中使用 ObjectIds 而不是 UUID 作為唯一的檔案 ID,但是如果需要,您可以使用 UUID。

在 Node.js 中,UUID 以 bson.Binary 類型的實例表示,該實例具有一個 getter,當您存取時會將二進位轉換為字串。Mongoose 將 UUID 儲存為 MongoDB 中子類型為 4 的二進位資料

const authorSchema = new Schema({
  _id: Schema.Types.UUID, // Can also do `_id: 'UUID'`
  name: String
});

const Author = mongoose.model('Author', authorSchema);

const bookSchema = new Schema({
  authorId: { type: Schema.Types.UUID, ref: 'Author' }
});
const Book = mongoose.model('Book', bookSchema);

const author = new Author({ name: 'Martin Fowler' });
console.log(typeof author._id); // 'string'
console.log(author.toObject()._id instanceof mongoose.mongo.BSON.Binary); // true

const book = new Book({ authorId: '09190f70-3d30-11e5-8814-0f4df9a59c41' });

若要建立 UUID,我們建議使用 Node 的內建 UUIDv4 產生器

const { randomUUID } = require('crypto');

const schema = new mongoose.Schema({
  docId: {
    type: 'UUID',
    default: () => randomUUID()
  }
});

BigInt

Mongoose 支援 JavaScript BigInts 作為 SchemaType。BigInts 在 MongoDB 中儲存為 64 位元整數 (BSON 類型 "long")

const questionSchema = new Schema({
  answer: BigInt
});
const Question = mongoose.model('Question', questionSchema);

const question = new Question({ answer: 42n });
typeof question.answer; // 'bigint'

Getters (getter)

Getters 類似於您架構中定義的路徑的虛擬屬性。例如,假設您想要將使用者個人資料圖片儲存為相對路徑,然後在您的應用程式中新增主機名稱。以下是您如何建構 userSchema 的方式

const root = 'https://s3.amazonaws.com/mybucket';

const userSchema = new Schema({
  name: String,
  picture: {
    type: String,
    get: v => `${root}${v}`
  }
});

const User = mongoose.model('User', userSchema);

const doc = new User({ name: 'Val', picture: '/123.png' });
doc.picture; // 'https://s3.amazonaws.com/mybucket/123.png'
doc.toObject({ getters: false }).picture; // '/123.png'

一般來說,您只會將 getters 用於原始路徑,而不是陣列或子文件。由於 getters 會覆寫存取 Mongoose 路徑時傳回的內容,因此在物件上宣告 getter 可能會移除該路徑的 Mongoose 變更追蹤。

const schema = new Schema({
  arr: [{ url: String }]
});

const root = 'https://s3.amazonaws.com/mybucket';

// Bad, don't do this!
schema.path('arr').get(v => {
  return v.map(el => Object.assign(el, { url: root + el.url }));
});

// Later
doc.arr.push({ key: String });
doc.arr[0]; // 'undefined' because every `doc.arr` creates a new array!

您應該在 url 字串上宣告 getter,如下所示,而不是像上面那樣在陣列上宣告 getter。如果您需要在巢狀文件或陣列上宣告 getter,請非常小心!

const schema = new Schema({
  arr: [{ url: String }]
});

const root = 'https://s3.amazonaws.com/mybucket';

// Good, do this instead of declaring a getter on `arr`
schema.path('arr.0.url').get(v => `${root}${v}`);

Schemas (綱要)

若要將路徑宣告為另一個架構,請將 type 設定為子架構的實例。

若要根據子架構的形狀設定預設值,只需設定一個預設值,該值會在文件建立期間設定之前,根據子架構的定義進行轉換。

const subSchema = new mongoose.Schema({
  // some schema definition here
});

const schema = new mongoose.Schema({
  data: {
    type: subSchema,
    default: {}
  }
});

建立自訂類型

Mongoose 也可以使用自訂 SchemaTypes 來擴充。在 外掛程式網站上搜尋相容的類型,例如 mongoose-longmongoose-int32mongoose-function

請在此處深入了解建立自訂 SchemaTypes 的資訊。

schema.path() 函數

schema.path() 函式會傳回給定路徑的已實例化架構類型。

const sampleSchema = new Schema({ name: { type: String, required: true } });
console.log(sampleSchema.path('name'));
// Output looks like:
/**
 * SchemaString {
 *   enumValues: [],
  *   regExp: null,
  *   path: 'name',
  *   instance: 'String',
  *   validators: ...
  */

您可以使用此函式來檢查給定路徑的架構類型,包括它擁有的驗證器和類型。

延伸閱讀

下一步

現在我們已經涵蓋了 SchemaTypes,讓我們來看看連線