サブドキュメント
サブドキュメントとは、他のドキュメントに埋め込まれたドキュメントのことです。Mongooseでは、これはスキーマを他のスキーマにネストできることを意味します。Mongooseには、サブドキュメントの2つの異なる概念があります:サブドキュメントの配列と、単一のネストされたサブドキュメントです。
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();
サブドキュメントには、トップレベルのドキュメントと同様に、save
およびvalidate
ミドルウェアがあります。親ドキュメントで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では、ネストされたパスはサブドキュメントとは微妙に異なります。たとえば、以下は2つのスキーマです。1つはchild
がサブドキュメントとして、もう1つは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);
これらの2つのスキーマは似ており、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
サブドキュメントのデフォルト
サブドキュメントのパスはデフォルトで未定義であり、サブドキュメントのパスをnullでない値に設定しない限り、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);
配列へのサブドキュメントの追加
push
、unshift
、addToSet
などの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!');
ドキュメント配列の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' })]
});
次へ
サブドキュメントについて説明したので、次はクエリについて見ていきましょう。