聚集后的猫鼬

时间:2018-11-20 18:58:47

标签: mongodb mongoose mongodb-query aggregation-framework mongoose-populate

在运行聚合管道并填充之后,我试图获取特定的数据模型,但是我却无法做到这一点。

最终所需的结果如下:

[
  {
    _accountId: "5beee0966d17bc42501f1234",
    name: "Company Name 1",
    contactEmail: "email1@email.com",
    contactName: "contact Name 1"
    reason: "Warranties",
    total: 1152,
    lineItems: [
      {
        _id: "5beee0966d17bc42501f5086",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf43929e7179a56e21382bc",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf4392fe7179a56e21382bd",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      }
    ]
  },
  {
    _accountId: "5beee0966d17bc42501f1235",
    name: "Company Name 2",
    contactEmail: "email2@email.com",
    contactName: "contact Name 2"
    reason: "Warranties",
    total: 1152,
    lineItems: [
      {
        _id: "5beee0966d17bc42501f5086",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf43929e7179a56e21382bc",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf4392fe7179a56e21382bd",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      }
    ]
  }
]

我正在从以下两个模型中收集这些数据:

保修

{
  _id: "5beee0966d17bc42501f5086",
  jobsiteAddressStreet: String,
  jobsiteAddressCity: String,
  jobsiteAddressState" String,
  jobsiteAddressZip: Number,
  warrantyFee: Number,
  _accountId: {
    type: Schema.Types.ObjectId,
    ref: "accounts"
  },
  payStatus: String
}

帐户

{
  _id: "5beee0966d17bc42501f1235",
  name: String,
  contactName: String,
  contactEmail: String
}

我当前的查询如下:

Warranty.aggregate([
    {
      $match: {
        payStatus: "Invoiced Next Billing Cycle"
      }
    },
    {
      $group: {
        _id: "$_accountId",
        total: {
          $sum: "$warrantyFee"
        },
        lineItems: {
          $push: {
            _id: "$_id",
            jobsiteAddress: {
              $concat: [
                "$jobsiteAddressStreet",
                " ",
                "$jobsiteAddressCity",
                ", ",
                "$jobsiteAddressState",
                " ",
                "$jobsiteAddressZip"
              ]
            },
            warrantyFee: "$warrantyFee"
          }
        }
      }
    },
    {
      $project: {
        reason: "Warranties",
        total: "$total",
        lineItems: "$lineItems"
      }
    }
  ])
    .then(warranties => {
      console.log(warranties);
      Account.populate(warranties, {
        path: "_id",
        select: "contactName contactEmail name"
      })
        .then(warranties => {
          res.send(warranties);
        })
        .catch(err => {
          res.status(422).send(err);
          throw err;
        });
    })
    .catch(err => {
      res.status(422).send(err);
      throw err;
    });

结果如下:

[
  {
    _id: {
      _id: "5bc39dfa331c0e2cb897b61e",
      name: "Company Name 1",
      contactEmail: "email1@email.com",
      contactName: "Contact Name 1"
    },
    reason: "Warranties",
    total: 1152,
    lineItems: [
      {
        _id: "5beee0966d17bc42501f5086",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf43929e7179a56e21382bc",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf4392fe7179a56e21382bd",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      }
    ]
  },
  {
    _id: {
      _id: "5bc39dfa331c0e2cb897b61e",
      name: "Company Name 2",
      contactEmail: "email2@email.com",
      contactName: "Contact Name 2"
    },
    reason: "Warranties",
    total: 1152,
    lineItems: [
      {
        _id: "5beee0966d17bc42501f5086",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf43929e7179a56e21382bc",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf4392fe7179a56e21382bd",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      }
    ]
  }
]

如您所见,这确实存在一些小问题。

  1. 它显示_id而不是_accountId。我默认使用此设置,因为每当我尝试在$ group中返回_accountId时,它将它标记为非累加器字段,而当我在$ project中执行此操作时,它不会显示。数据集必须按“保修”模型中的_accountId分组。
  2. 我希望将其他字段(contactName,contactEmail,name)添加到顶级对象,而不是尽可能创建子文档。这可能是简单或不可能的,因为我对填充并不十分熟悉,但却找不到任何直接回答我问题的方法。

此操作的目标是获取返回的对象并使用对象数组为另一个集合进行大量文档创建。

-回答我的特定用例-

Warranty.aggregate([
    {
      $match: {
        payStatus: "Invoiced Next Billing Cycle"
      }
    },
    {
      $group: {
        _id: "$_accountId",
        total: {
          $sum: "$warrantyFee"
        },
        lineItems: {
          $push: {
            _id: "$_id",
            jobsiteAddress: {
              $concat: [
                "$jobsiteAddressStreet",
                " ",
                "$jobsiteAddressCity",
                ", ",
                "$jobsiteAddressState",
                " ",
                "$jobsiteAddressZip"
              ]
            },
            warrantyFee: "$warrantyFee"
          }
        }
      }
    },
    {
      $lookup: {
        from: Account.collection.name,
        localField: "_id",
        foreignField: "_id",
        as: "accounts"
      }
    },
    {
      $unwind: "$accounts"
    },
    {
      $project: {
        lineItems: "$lineItems",
        reason: "Warranties",
        total: "$total",
        type: "Invoice",
        date: new Date(),
        company: "$accounts.name",
        contactName: "$accounts.contactName",
        contactEmail: "$accounts.contactEmail"
      }
    },
    {
      $addFields: {
        _accountId: "$_id"
      }
    },
    {
      $project: {
        _id: 0
      }
    }
  ])

这给了我结果:

[
  {
    lineItems: [
      {
        _id: "5be203eb3afd8098d4988152",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      }
    ],
    reason: "Warranties",
    total: 384,
    type: "Invoice",
    date: "2018-11-21T14:08:15.052Z",
    company: "Company Name 1",
    contactName: "Contact Name 1",
    contactEmail: "email1@email.com",
    _accountId: "5be203eb3afd8098d4988152",
    referenceNumber: 1542809296615
  },
  {
    lineItems: [
      {
        _id: "5beee0966d17bc42501f5086",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf43929e7179a56e21382bc",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      },
      {
        _id: "5bf4392fe7179a56e21382bd",
        jobsiteAddress: "1234 Street Southwest Sunnyville, Wyoming 12345",
        warrantyFee: 384
      }
    ],
    reason: "Warranties",
    total: 1152,
    type: "Invoice",
    date: "2018-11-21T14:08:15.052Z",
    company: "Company Name 2",
    contactName: "Contact Name 2",
    contactEmail: "email2@email.com",
    _accountId: "5bc39dfa331c0e2cb897b61e",
    referenceNumber: 1542809295680
  }
]

2 个答案:

答案 0 :(得分:4)

因此,当您要求“填充”汇总结果时,您实际上在这里缺少一些概念。通常,这不是您实际要做的,而是要解释要点:

  1. aggregate()的输出不同于Model.find()或类似的动作,因为此处的目的是“重塑结果”。这基本上意味着您不再将用作聚合来源的模型视为输出模型。即使您仍然在输出上保持完全相同的文档结构,甚至是这样,但在您的情况下,输出显然仍然与源文档不同。

    无论如何,它不再是您从中采购的Warranty模型的实例,而只是一个普通对象。我们可以稍后再解决。

  2. 这里的重点可能是populate()还是有点“老帽子” 。实际上,这只是在实施的早期就添加到Mongoose的便捷功能。它真正要做的就是对单独集合中与相关的数据执行“另一个查询”,然后将内存中的结果合并到原始集合输出中。

    由于许多原因,这在大多数情况下并不是很有效,甚至不是理想的。与流行的误解相反,这不是实际上是“加入”。

    对于真正的“联接”,您实际上使用了$lookup聚合管道阶段,MongoDB使用该阶段从另一个集合返回匹配项。与populate()不同,这实际上是在对服务器的单个请求中使用单个响应完成的。这样可以避免网络开销,通常更快,并且通过“真实联接”可以执行populate()无法完成的工作。

改为使用$ lookup

此处非常缺少的 quick 版本是:返回结果后,您不尝试在populate()中尝试.then(),而是添加了$lookup进入管道:

  { "$lookup": {
    "from": Account.collection.name,
    "localField": "_id",
    "foreignField": "_id",
    "as": "accounts"
  }},
  { "$unwind": "$accounts" },
  { "$project": {
    "_id": "$accounts",
    "total": 1,
    "lineItems": 1
  }}

请注意,这里存在一个约束,因为$lookup的输出总是 个数组。只有一个或多个相关项目要作为输出获取并不重要。管线阶段将从当前提供的文档中寻找"localField"的值,并使用它来匹配指定的"foreignField"中的值。在这种情况下,它是从集合$group到外部集合_id的{​​{1}}。

由于输出如上所述是总是一个数组,因此对此实例进行处理的最有效方法是直接在{{3}之后添加一个$unwind阶段}。所有这些都将为目标数组中返回的每个项目返回一个新文档,在这种情况下,您希望它是一个。如果外部集合中的_id不匹配,则不匹配的结果将被删除。

请注意,这实际上是一种优化的模式,如核心文档中的$lookup中所述。这里发生了一件特别的事情,其中​​$lookup + $unwind Coalescence指令实际上以一种有效的方式合并到了$unwind操作中。您可以在那了解更多信息。

使用填充

从以上内容中,您应该可以基本了解为什么_id在这里是错误的事情。除了输出不再由populate()模型对象组成的基本事实之外,该模型实际上仅知道关于Warranty属性中描述的异物,但这些异物在输出中根本不存在。

现在,您可以 实际定义一个模型,该模型可用于将输出对象显式转换为已定义的输出类型。一个简短的演示将涉及为您的应用程序添加代码,例如:

_accountId

然后可以使用这个新的// Special models const outputSchema = new Schema({ _id: { type: Schema.Types.ObjectId, ref: "Account" }, total: Number, lineItems: [{ address: String }] }); const Output = mongoose.model('Output', outputSchema, 'dontuseme'); 模型来将生成的普通JavaScript对象“投射”到Mongoose文档中,以便实际上可以调用诸如$lookup之类的方法:

Output

由于// excerpt result2 = result2.map(r => new Output(r)); // Cast to Output Mongoose Documents // Call populate on the list of documents result2 = await Output.populate(result2, { path: '_id' }) log(result2); 定义了一个架构,该架构知道文档Output字段上的“引用”,因此_id知道它需要做什么并返回项目

请注意,因为这实际上会生成另一个查询。即:

Model.populate()

第一行是聚合输出,然后您再次与服务器联系以返回相关的Mongoose: warranties.aggregate([ { '$match': { payStatus: 'Invoiced Next Billing Cycle' } }, { '$group': { _id: '$_accountId', total: { '$sum': '$warrantyFee' }, lineItems: { '$push': { _id: '$_id', address: { '$trim': { input: { '$reduce': { input: { '$objectToArray': '$address' }, initialValue: '', in: { '$concat': [ '$$value', ' ', [Object] ] } } }, chars: ' ' } } } } } } ], {}) Mongoose: accounts.find({ _id: { '$in': [ ObjectId("5bf4b591a06509544b8cf75c"), ObjectId("5bf4b591a06509544b8cf75b") ] } }, { projection: {} }) 模型条目。

摘要

这些是您的选择,但是应该很清楚,现代的方法是使用Model.populate()并获得一个真正的“ join” ,而不是{{ 1}}实际上在做。

其中列出了所有这些方法在实践中实际工作方式的完整说明。这里采用了一些艺术许可,因此所表示的模型可能与您拥有的不完全相同,但是有足够的方式以可重复的方式演示基本概念:

Account

完整的输出:

populate()

答案 1 :(得分:0)

  1. 它显示_accountId的_id insead,因为当您使用$ group时, 结果按指定的_accountId分组,因此它成为 文件的新_id。
  2. 有两种可能的解决方案,用于将contactName,contactEmail和name移至顶层:
    • 一个人正在用javascript处理它。为此,可以使用函数'map()'。
    • 另一种解决方案是在聚合管道中使用$ lookup来填充同一mongoDB查询中的文档,并且在$ lookup之后,您必须再次使用$ project来根据需要构建输出文档。