阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 嵌套文档与子文档操作

嵌套文档与子文档操作

作者:陈川 阅读数:23370人阅读 分类: MongoDB

嵌套文档与子文档的基本概念

Mongoose中的嵌套文档是指在一个文档中直接嵌入另一个文档的结构。这种设计模式允许将相关数据组织在一起,无需额外的集合或引用。子文档则是嵌套文档的另一种表现形式,通常作为父文档的一个属性存在。

const childSchema = new mongoose.Schema({
  name: String,
  age: Number
});

const parentSchema = new mongoose.Schema({
  children: [childSchema],
  address: String
});

在这个例子中,childSchema被嵌套在parentSchema中作为子文档数组。Mongoose会为子文档自动创建_id字段,除非显式禁用。

创建嵌套文档

创建包含嵌套文档的文档有几种方式。最直接的方法是在创建父文档时同时指定子文档内容:

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

const newParent = new Parent({
  address: '123 Main St',
  children: [
    { name: 'Alice', age: 8 },
    { name: 'Bob', age: 5 }
  ]
});

也可以先创建子文档实例,再将其添加到父文档中:

const child1 = { name: 'Charlie', age: 10 };
const child2 = { name: 'Diana', age: 7 };

const anotherParent = new Parent({
  address: '456 Oak Ave',
  children: [child1, child2]
});

查询嵌套文档

查询嵌套文档时,Mongoose提供了多种操作符和方法。最基本的查询可以通过点符号访问嵌套字段:

Parent.find({ 'children.age': { $gt: 6 } }, (err, parents) => {
  console.log(parents);
});

使用$elemMatch可以执行更复杂的查询:

Parent.find({
  children: {
    $elemMatch: {
      age: { $gt: 5 },
      name: /^A/
    }
  }
});

更新嵌套文档

更新嵌套文档有多种方法。可以使用传统的update操作:

Parent.updateOne(
  { _id: parentId, 'children._id': childId },
  { $set: { 'children.$.age': 9 } },
  (err, result) => {
    // 处理结果
  }
);

Mongoose还提供了更便捷的findOneAndUpdate

Parent.findOneAndUpdate(
  { _id: parentId, 'children._id': childId },
  { $inc: { 'children.$.age': 1 } },
  { new: true }
).then(updatedParent => {
  console.log(updatedParent);
});

删除嵌套文档

从父文档中删除子文档可以使用$pull操作符:

Parent.updateOne(
  { _id: parentId },
  { $pull: { children: { _id: childId } } },
  (err, result) => {
    // 处理结果
  }
);

如果要删除所有符合条件的子文档:

Parent.updateMany(
  {},
  { $pull: { children: { age: { $lt: 3 } } } },
  { multi: true }
);

嵌套文档的验证

Mongoose会自动应用子文档模式的验证规则。例如,如果子文档模式要求name字段:

const strictChildSchema = new mongoose.Schema({
  name: { type: String, required: true },
  age: Number
});

const strictParentSchema = new mongoose.Schema({
  children: [strictChildSchema]
});

尝试保存缺少name的子文档会触发验证错误:

const invalidParent = new Parent({
  children: [{ age: 4 }]  // 缺少必填的name字段
});

invalidParent.save(err => {
  console.log(err);  // 验证错误
});

嵌套文档与中间件

可以在嵌套文档上使用Mongoose中间件。例如,保存前的预处理:

childSchema.pre('save', function(next) {
  if (this.age < 0) {
    this.age = 0;
  }
  next();
});

这个中间件会在每个子文档保存前执行,确保年龄不为负数。

嵌套文档数组操作

Mongoose提供了对嵌套文档数组的特殊操作方法。push方法可以添加新子文档:

Parent.findById(parentId, (err, parent) => {
  parent.children.push({ name: 'Eva', age: 6 });
  parent.save();
});

使用id方法可以快速查找特定子文档:

const parent = await Parent.findById(parentId);
const child = parent.children.id(childId);
console.log(child);

嵌套文档与填充(Population)的区别

虽然嵌套文档和填充都能建立文档间关系,但工作机制不同。填充使用引用和额外查询:

const refChildSchema = new mongoose.Schema({
  name: String,
  age: Number
});

const refParentSchema = new mongoose.Schema({
  children: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Child' }]
});

相比之下,嵌套文档将所有数据存储在同一个文档中,查询时不需要额外操作。

嵌套文档的性能考量

嵌套文档适合以下场景:

  • 子文档数量有限且不会无限增长
  • 子文档主要与父文档一起访问
  • 需要原子性更新父文档和子文档

不适合的场景:

  • 子文档数量可能非常大
  • 需要单独查询或更新子文档
  • 多个父文档共享相同的子文档

嵌套文档的深度限制

Mongoose默认不限制嵌套深度,但可以自定义验证:

const deepSchema = new mongoose.Schema({
  level1: {
    level2: {
      level3: {
        value: String
      }
    }
  }
});

function validateDepth(doc, maxDepth) {
  // 实现深度验证逻辑
}

deepSchema.pre('save', function(next) {
  if (validateDepth(this, 3)) {
    next(new Error('Maximum nesting depth exceeded'));
  } else {
    next();
  }
});

嵌套文档的索引

可以为嵌套字段创建索引以提高查询性能:

parentSchema.index({ 'children.name': 1 });
parentSchema.index({ 'children.age': -1 });

复合索引也同样适用:

parentSchema.index({ 'children.name': 1, 'children.age': -1 });

嵌套文档与原子操作

Mongoose保证对单个文档(包括其嵌套文档)的更新是原子的。例如,可以原子性地更新父文档和子文档:

Parent.findOneAndUpdate(
  { _id: parentId },
  {
    $set: { address: 'New Address' },
    $inc: { 'children.$.age': 1 }
  },
  { new: true }
);

嵌套文档的模式设计技巧

设计嵌套文档模式时考虑以下因素:

  1. 访问模式:哪些字段经常一起查询
  2. 更新频率:哪些字段经常被单独更新
  3. 大小限制:MongoDB文档最大16MB
// 好的设计:经常一起访问的数据放在一起
const userSchema = new mongoose.Schema({
  profile: {
    name: String,
    avatar: String,
    bio: String
  },
  preferences: {
    theme: String,
    notifications: Boolean
  }
});

嵌套文档的迁移策略

当需要修改嵌套文档结构时,可以考虑以下迁移方法:

  1. 批量更新所有文档:
Parent.updateMany(
  {},
  { $rename: { 'children.grade': 'children.level' } }
);
  1. 使用迁移脚本处理复杂情况:
async function migrateChildren() {
  const parents = await Parent.find({});
  
  for (const parent of parents) {
    parent.children.forEach(child => {
      if (child.grade) {
        child.level = convertGradeToLevel(child.grade);
        delete child.grade;
      }
    });
    await parent.save();
  }
}

嵌套文档与事务

在MongoDB事务中操作嵌套文档时,整个文档被视为一个原子单元:

const session = await mongoose.startSession();
session.startTransaction();

try {
  const parent = await Parent.findById(parentId).session(session);
  parent.children.push(newChild);
  await parent.save();
  
  await session.commitTransaction();
} catch (error) {
  await session.abortTransaction();
  throw error;
} finally {
  session.endSession();
}

嵌套文档的虚拟属性

可以为嵌套文档添加虚拟属性:

childSchema.virtual('ageGroup').get(function() {
  if (this.age < 5) return 'Toddler';
  if (this.age < 12) return 'Child';
  return 'Teen';
});

const parent = await Parent.findById(parentId);
console.log(parent.children[0].ageGroup);  // 根据年龄返回分组

嵌套文档的JSON序列化

控制嵌套文档的JSON输出:

childSchema.set('toJSON', {
  transform: (doc, ret) => {
    ret.id = ret._id;
    delete ret._id;
    delete ret.__v;
    return ret;
  }
});

parentSchema.set('toJSON', {
  virtuals: true
});

嵌套文档的默认值

为嵌套文档设置默认值:

const settingsSchema = new mongoose.Schema({
  notifications: { type: Boolean, default: true },
  theme: { type: String, default: 'light' }
});

const userSchema = new mongoose.Schema({
  settings: { type: settingsSchema, default: () => ({}) }
});

嵌套文档的条件验证

根据父文档状态验证子文档:

parentSchema.path('children').validate(function(children) {
  if (this.type === 'large_family' && children.length < 3) {
    return false;
  }
  return true;
}, 'Large families must have at least 3 children');

嵌套文档的查询性能优化

优化嵌套文档查询的方法:

  1. 使用投影只返回需要的字段:
Parent.find({}, 'address children.name');
  1. 对常用查询条件创建索引:
parentSchema.index({ 'children.age': 1, address: 1 });
  1. 使用$slice限制返回的数组元素:
Parent.findById(parentId, { children: { $slice: 5 } });

嵌套文档与GraphQL

在GraphQL中处理嵌套文档:

const ParentType = new GraphQLObjectType({
  name: 'Parent',
  fields: () => ({
    address: { type: GraphQLString },
    children: {
      type: new GraphQLList(ChildType),
      resolve(parent) {
        return parent.children;
      }
    }
  })
});

const ChildType = new GraphQLObjectType({
  name: 'Child',
  fields: {
    name: { type: GraphQLString },
    age: { type: GraphQLInt }
  }
});

嵌套文档的版本控制

实现嵌套文档的版本控制:

const versionedChildSchema = new mongoose.Schema({
  name: String,
  age: Number,
  version: { type: Number, default: 0 }
});

childSchema.pre('save', function(next) {
  if (this.isModified()) {
    this.version += 1;
  }
  next();
});

嵌套文档的并行处理

安全地并行处理嵌套文档:

async function updateChildAge(parentId, childId, newAge) {
  const parent = await Parent.findById(parentId);
  const child = parent.children.id(childId);
  
  if (!child) throw new Error('Child not found');
  
  child.age = newAge;
  await parent.save();
}

嵌套文档的批量插入

高效插入多个嵌套文档:

async function addMultipleChildren(parentId, newChildren) {
  await Parent.findByIdAndUpdate(
    parentId,
    { $push: { children: { $each: newChildren } } },
    { new: true }
  );
}

嵌套文档的引用计数

在嵌套文档中实现引用计数:

const tagSchema = new mongoose.Schema({
  name: String,
  count: { type: Number, default: 0 }
});

const postSchema = new mongoose.Schema({
  tags: [tagSchema]
});

postSchema.pre('save', function(next) {
  this.tags.forEach(tag => {
    tag.count = this.tags.filter(t => t.name === tag.name).length;
  });
  next();
});

嵌套文档的类型转换

处理嵌套文档中的类型转换:

childSchema.path('age').set(function(v) {
  return parseInt(v, 10) || 0;
});

const parent = new Parent({
  children: [{ age: '10' }]  // 自动转换为数字10
});

嵌套文档的加密

加密敏感嵌套字段:

const crypto = require('crypto');
const secret = 'my-secret-key';

childSchema.pre('save', function(next) {
  if (this.isModified('ssn')) {
    const cipher = crypto.createCipher('aes-256-cbc', secret);
    this.ssn = cipher.update(this.ssn, 'utf8', 'hex') + cipher.final('hex');
  }
  next();
});

childSchema.methods.decryptSsn = function() {
  const decipher = crypto.createDecipher('aes-256-cbc', secret);
  return decipher.update(this.ssn, 'hex', 'utf8') + decipher.final('utf8');
};

嵌套文档的国际化

实现嵌套文档的多语言支持:

const i18nStringSchema = new mongoose.Schema({
  en: String,
  es: String,
  fr: String
});

const productSchema = new mongoose.Schema({
  name: i18nStringSchema,
  description: i18nStringSchema
});

productSchema.methods.getName = function(lang) {
  return this.name[lang] || this.name.en;
};

嵌套文档的审计日志

跟踪嵌套文档的变更历史:

const auditLogSchema = new mongoose.Schema({
  timestamp: { type: Date, default: Date.now },
  operation: String,
  data: mongoose.Schema.Types.Mixed
});

const auditableSchema = new mongoose.Schema({
  content: String,
  logs: [auditLogSchema]
});

auditableSchema.pre('save', function(next) {
  if (this.isModified()) {
    this.logs.push({
      operation: this.isNew ? 'create' : 'update',
      data: this.getChanges()
    });
  }
  next();
});

嵌套文档的缓存策略

优化嵌套文档的缓存:

const cache = new Map();

parentSchema.statics.findByIdWithCache = async function(id) {
  if (cache.has(id)) {
    return cache.get(id);
  }
  
  const parent = await this.findById(id).lean();
  cache.set(id, parent);
  return parent;
};

parentSchema.post('save', function(doc) {
  cache.set(doc._id.toString(), doc.toObject());
});

嵌套文档的测试策略

测试嵌套文档的示例:

describe('Parent Model', () => {
  it('should validate child age', async () => {
    const parent = new Parent({
      children: [{ name: 'Test', age: -1 }]
    });
    
    await expect(parent.save()).rejects.toThrow();
  });

  it('should update nested child', async () => {
    const parent = await Parent.create({ /* ... */ });
    await Parent.updateOne(
      { _id: parent._id, 'children._id': parent.children[0]._id },
      { $set: { 'children.$.name': 'Updated' } }
    );
    
    const updated = await Parent.findById(parent._id);
    expect(updated.children[0].name).toBe('Updated');
  });
});

嵌套文档的迁移到独立集合

将嵌套文档迁移为独立集合的策略:

async function migrateChildrenToCollection() {
  const parents = await Parent.find({});
  const Child = mongoose.model('Child', childSchema);
  
  for (const parent of parents) {
    const childDocs = parent.children.map(child => ({
      ...child.toObject(),
      parent: parent._id
    }));
    
    await Child.insertMany(childDocs);
    await Parent.updateOne(
      { _id: parent._id },
      { $set: { children: [] } }
    );
  }
}

嵌套文档的聚合操作

在聚合管道中处理嵌套文档:

Parent.aggregate([
  { $unwind: '$children' },
  { $match: { 'children.age': { $gte: 5 } } },
  { $group: {
    _id: '$_id',
    address: { $first: '$address' },
    childrenCount: { $sum: 1 }
  }}
]);

嵌套文档的地理空间查询

在嵌套文档中使用地理空间查询:

const locationSchema = new mongoose.Schema({
  type: { type: String, default: 'Point' },
  coordinates: { type: [Number], index: '2dsphere' }
});

const placeSchema = new mongoose.Schema({
  name: String,
  locations: [locationSchema]
});

placeSchema.index({ 'locations': '2dsphere' });

Place.find({
  locations: {
    $near: {
      $geometry: {
        type: 'Point',
        coordinates: [longitude, latitude]
      },
      $maxDistance: 1000
    }
  }
});

嵌套文档的全文搜索

实现嵌套文档的全文搜索:

parentSchema.index({
  'children.name': 'text',
  'children.description': 'text'
});

Parent.find(
  { $text: { $search: 'keyword' } },
  { score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } });

嵌套文档的权限控制

基于角色的嵌套文档访问控制:

parentSchema.methods.filterChildrenForRole = function(role) {
  if (role === 'admin') {
    return this.children;
  } else {
    return this.children.map(child => ({
      name: child.name,
      age: child.age
      // 隐藏敏感字段
    }));
  }
};

嵌套文档的乐观并发控制

实现嵌套文档的版本控制:

childSchema.add({
  version: { type: Number, default: 0 }
});

childSchema.pre('save', function(next) {
  if (this.isModified()) {
    this.version += 1;
  }
  next();
});

async function updateChildWithConflictDetection(parentId, childId, update) {
  const parent = await Parent.findById(parentId);
  const child = parent.children.id(childId);
  

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌