子文檔

子文檔是嵌入在其他文檔中的文檔。在 Mongoose 中,這表示您可以將結構描述巢狀於其他結構描述中。Mongoose 有兩個不同的子文檔概念:子文檔陣列和單一巢狀子文檔。

const childSchema = new Schema({ name: 'string' });

const parentSchema = new Schema({
  // Array of subdocuments
  children: [childSchema],
  // Single nested subdocuments
  child: childSchema
});

請注意,填充的文檔在 Mongoose 中不是子文檔。子文檔資料嵌入在最上層文檔中。參考文檔是獨立的最上層文檔。

const childSchema = new Schema({ name: 'string' });
const Child = mongoose.model('Child', childSchema);

const parentSchema = new Schema({
  child: {
    type: mongoose.ObjectId,
    ref: 'Child'
  }
});
const Parent = mongoose.model('Parent', parentSchema);

const doc = await Parent.findOne().populate('child');
// NOT a subdocument. `doc.child` is a separate top-level document.
doc.child;

什麼是子文檔?

子文檔與一般文檔類似。巢狀結構描述可以有中介軟體自訂驗證邏輯、虛擬屬性以及任何其他最上層結構描述可以使用的功能。主要區別在於子文檔不會個別儲存,它們會在儲存其最上層父級文檔時儲存。

const Parent = mongoose.model('Parent', parentSchema);
const parent = new Parent({ children: [{ name: 'Matt' }, { name: 'Sarah' }] });
parent.children[0].name = 'Matthew';

// `parent.children[0].save()` is a no-op, it triggers middleware but
// does **not** actually save the subdocument. You need to save the parent
// doc.
await parent.save();

子文檔具有 savevalidate 中介軟體,就像最上層文檔一樣。在父級文檔上呼叫 save() 會觸發其所有子文檔的 save() 中介軟體,validate() 中介軟體也是如此。

childSchema.pre('save', function(next) {
  if ('invalid' == this.name) {
    return next(new Error('#sadpanda'));
  }
  next();
});

const parent = new Parent({ children: [{ name: 'invalid' }] });
try {
  await parent.save();
} catch (err) {
  err.message; // '#sadpanda'
}

子文檔的 pre('save')pre('validate') 中介軟體會在最上層文檔的 pre('save') 之前執行,但在最上層文檔的 pre('validate') 中介軟體之後執行。這是因為在 save() 之前進行驗證實際上是一段內建的中介軟體。

// Below code will print out 1-4 in order
const childSchema = new mongoose.Schema({ name: 'string' });

childSchema.pre('validate', function(next) {
  console.log('2');
  next();
});

childSchema.pre('save', function(next) {
  console.log('3');
  next();
});

const parentSchema = new mongoose.Schema({
  child: childSchema
});

parentSchema.pre('validate', function(next) {
  console.log('1');
  next();
});

parentSchema.pre('save', function(next) {
  console.log('4');
  next();
});

子文檔與巢狀路徑

在 Mongoose 中,巢狀路徑與子文檔略有不同。例如,以下是兩個結構描述:一個將 child 作為子文檔,另一個將 child 作為巢狀路徑。

// Subdocument
const subdocumentSchema = new mongoose.Schema({
  child: new mongoose.Schema({ name: String, age: Number })
});
const Subdoc = mongoose.model('Subdoc', subdocumentSchema);

// Nested path
const nestedSchema = new mongoose.Schema({
  child: { name: String, age: Number }
});
const Nested = mongoose.model('Nested', nestedSchema);

這兩個結構描述看起來相似,並且 MongoDB 中的文檔在這兩個結構描述中都具有相同的結構。但是存在一些 Mongoose 特有的差異

首先,Nested 的實例永遠不會有 child === undefined。您始終可以設定 child 的子屬性,即使您沒有設定 child 屬性也是如此。但是 Subdoc 的實例可能會具有 child === undefined

const doc1 = new Subdoc({});
doc1.child === undefined; // true
doc1.child.name = 'test'; // Throws TypeError: cannot read property...

const doc2 = new Nested({});
doc2.child === undefined; // false
console.log(doc2.child); // Prints 'MongooseDocument { undefined }'
doc2.child.name = 'test'; // Works

子文檔預設值

子文檔路徑預設為未定義,並且 Mongoose 不會套用子文檔預設值,除非您將子文檔路徑設定為非空值。

const subdocumentSchema = new mongoose.Schema({
  child: new mongoose.Schema({
    name: String,
    age: {
      type: Number,
      default: 0
    }
  })
});
const Subdoc = mongoose.model('Subdoc', subdocumentSchema);

// Note that the `age` default has no effect, because `child`
// is `undefined`.
const doc = new Subdoc();
doc.child; // undefined

但是,如果您將 doc.child 設定為任何物件,則 Mongoose 會在必要時套用 age 預設值。

doc.child = {};
// Mongoose applies the `age` default:
doc.child.age; // 0

Mongoose 會遞迴地套用預設值,這表示如果您想確保 Mongoose 套用子文檔預設值,則有一個不錯的解決方法:將子文檔路徑預設為空物件。

const childSchema = new mongoose.Schema({
  name: String,
  age: {
    type: Number,
    default: 0
  }
});
const subdocumentSchema = new mongoose.Schema({
  child: {
    type: childSchema,
    default: () => ({})
  }
});
const Subdoc = mongoose.model('Subdoc', subdocumentSchema);

// Note that Mongoose sets `age` to its default value 0, because
// `child` defaults to an empty object and Mongoose applies
// defaults to that empty object.
const doc = new Subdoc();
doc.child; // { age: 0 }

尋找子文檔

每個子文檔預設都有一個 _id。Mongoose 文檔陣列有一個特殊的 id 方法,用於搜尋文檔陣列以尋找具有給定 _id 的文檔。

const doc = parent.children.id(_id);

將子文檔新增到陣列

pushunshiftaddToSet 等 MongooseArray 方法會將引數透明地轉換為其正確的類型

const Parent = mongoose.model('Parent');
const parent = new Parent();

// create a comment
parent.children.push({ name: 'Liesl' });
const subdoc = parent.children[0];
console.log(subdoc); // { _id: '501d86090d371bab2c0341c5', name: 'Liesl' }
subdoc.isNew; // true

await parent.save();
console.log('Success!');

您也可以使用 Document Arrays 的 create() 方法建立子文檔,而無需將其新增到陣列。

const newdoc = parent.children.create({ name: 'Aaron' });

移除子文檔

每個子文檔都有自己的 deleteOne 方法。對於陣列子文檔,這相當於在子文檔上呼叫 .pull()。對於單一巢狀子文檔,deleteOne() 相當於將子文檔設定為 null

// Equivalent to `parent.children.pull(_id)`
parent.children.id(_id).deleteOne();
// Equivalent to `parent.child = null`
parent.child.deleteOne();

await parent.save();
console.log('the subdocs were removed');

子文檔的父級

有時,您需要取得子文檔的父級。您可以使用 parent() 函數存取父級。

const schema = new Schema({
  docArr: [{ name: String }],
  singleNested: new Schema({ name: String })
});
const Model = mongoose.model('Test', schema);

const doc = new Model({
  docArr: [{ name: 'foo' }],
  singleNested: { name: 'bar' }
});

doc.singleNested.parent() === doc; // true
doc.docArr[0].parent() === doc; // true

如果您有深度巢狀的子文檔,則可以使用 ownerDocument() 函數存取最上層文檔。

const schema = new Schema({
  level1: new Schema({
    level2: new Schema({
      test: String
    })
  })
});
const Model = mongoose.model('Test', schema);

const doc = new Model({ level1: { level2: 'test' } });

doc.level1.level2.parent() === doc; // false
doc.level1.level2.parent() === doc.level1; // true
doc.level1.level2.ownerDocument() === doc; // true

陣列的替代宣告語法

如果您使用物件陣列建立結構描述,Mongoose 會自動將該物件轉換為結構描述

const parentSchema = new Schema({
  children: [{ name: 'string' }]
});
// Equivalent
const parentSchema = new Schema({
  children: [new Schema({ name: 'string' })]
});

下一步

現在我們已經介紹了子文檔,讓我們來看看查詢