You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将Mongoose关联文档字段移至主文档并返回扁平化DTO

问题

需要将Mongoose中User模型关联的City、Country子文档的cityName、countryName字段迁移至主文档,返回无嵌套结构的单一DTO。以下是相关代码、当前返回结果及期望格式:

示例代码(data.js)

const mongoose = require('mongoose');

//city
const citySchema = new mongoose.Schema({
    cityName: { type: String, required: true, unique: true },
});

const City = mongoose.model('City', citySchema);

//country
const countrySchema = new mongoose.Schema({
    countryName: { type: String, required: true, unique: true },
});

const Country = mongoose.model('Country', countrySchema);

//user
const userSchema = new mongoose.Schema({
    username: { type: String, required: true },
    city: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'City',
        required: true,
    },
    country: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Country',
        required: true,
    },
    createdAt: { type: Date, required: true },
    updatedAt: { type: Date, required: true },
});

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


function getUser(id) {
    return User.findById(id)
        .populate('city')
        .populate('country')
        .exec();
};

当前接口返回JSON

{
  "_id": "6321ac3d14a57c2716f7f4a0",
  "name": "David",
  "city": {
      "_id": "63218ce557336b03540c9ce9",
      "cityName": "New York",
      "__v": 0
  },
  "country": {
      "_id": "632185bbe499d5505cafdcbc",
      "countryName": "USA",
      "__v": 0
  },
  "createdAt": "2022-09-14T10:26:05.000Z",
  "__v": 0
}

期望返回JSON格式

{
  "_id": "6321ac3d14a57c2716f7f4a0",
  "username": "David",
  "cityName": "New York",
  "countryName": "USA",
  "createdAt": "2022-09-14T10:26:05.000Z",
}
解决方案

方法1:查询后手动重组数据

获取populate后的用户文档后,直接提取需要的字段并重组结构:

async function getUser(id) {
  const user = await User.findById(id)
    .populate('city')
    .populate('country')
    .exec();
  
  return {
    _id: user._id,
    username: user.username,
    cityName: user.city.cityName,
    countryName: user.country.countryName,
    createdAt: user.createdAt
  };
};

这种方式逻辑简单直观,适合字段较少的场景,缺点是需要手动维护字段映射关系。

方法2:结合lean()与投影简化处理

用lean()返回普通JS对象,配合select过滤不必要的字段,再提取嵌套字段到顶层:

async function getUser(id) {
  return User.findById(id)
    .populate({
      path: 'city',
      select: 'cityName -_id' // 仅保留cityName,排除_id
    })
    .populate({
      path: 'country',
      select: 'countryName -_id' // 仅保留countryName,排除_id
    })
    .lean() // 提升性能,返回普通JS对象而非Mongoose文档
    .then(user => {
      const { city, country, ...rest } = user;
      return {
        ...rest,
        cityName: city.cityName,
        countryName: country.countryName
      };
    });
};

lean()能降低内存开销,select减少数据传输量,整体效率更高。

方法3:使用聚合管道在数据库层处理

通过聚合阶段直接在数据库完成字段重组,无需应用层额外计算:

async function getUser(id) {
  return User.aggregate([
    // 匹配目标用户
    { $match: { _id: mongoose.Types.ObjectId(id) } },
    // 关联City集合,仅获取cityName
    {
      $lookup: {
        from: 'cities', // 注意这里是集合的实际复数名称
        localField: 'city',
        foreignField: '_id',
        as: 'city'
      }
    },
    // 关联Country集合,仅获取countryName
    {
      $lookup: {
        from: 'countries', // 集合实际复数名称
        localField: 'country',
        foreignField: '_id',
        as: 'country'
      }
    },
    // 展开$lookup返回的数组
    { $unwind: '$city' },
    { $unwind: '$country' },
    // 投影并重命名字段到顶层
    {
      $project: {
        _id: 1,
        username: 1,
        cityName: '$city.cityName',
        countryName: '$country.countryName',
        createdAt: 1
      }
    }
  ]).then(results => results[0]); // 聚合返回数组,取第一个结果
};

聚合管道适合复杂数据重组场景,所有处理在数据库端完成,减少应用层压力。


内容的提问来源于stack exchange,提问作者Howard Hee

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 08:20:24