在阵列上填充+聚合

时间:2018-11-13 02:57:12

标签: node.js mongodb mongoose

我正在将猫鼬(5.x.x)与填充函数一起使用引用填充数组(餐)。  在该数组中,我需要将价格(填充结果)和数量(基本模式的一部分)相乘。

我的填充结果如下:

{
    "_id": "5bea354235711482876f8fa8",
    "meals": [
        {
            "meal": {
                "_id": "5be93c7074488c77b10fba00",
                "name": "Chicken Nuggets",
                "price": 3
            },
            "quantity": 12
        },
        {
            "meal": {
                "_id": "5be93c9274488c77b10fba01",
                "name": "Beef Burger",
                "price": 6
            },
            "quantity": 4
        }
    ],
    "__v": 0
}

目标是在此结果集中添加“总价”,但我找不到任何优雅的方法。  我想避免在查询之外处理数据。

感谢您的帮助,

1 个答案:

答案 0 :(得分:2)

因此,有两种方法可以做到这一点。

使用$ lookup

您基本上想从其他集合中获取“相关”数据,并将其与现有数组项“合并”。由于$lookup不能做到这一点,因此您实际上不能只是“瞄准”现有的数组,但是它可以编写另一个数组,然后可以将它们“合并”在一起:

let result1 = await Order.aggregate([
  { "$lookup": {
    "from": Meal.collection.name,
    "foreignField": "_id",
    "localField": "meals.meal",
    "as": "mealitems"
  }},
  { "$project": {
    "meals": {
      "$map": {
        "input": "$meals",
        "in": {
          "meal": {
            "$arrayElemAt": [
              "$mealitems",
              { "$indexOfArray": [ "$mealitems._id", "$$this.meal" ] }
            ]
          },
          "quantity": "$$this.quantity",
          "totalPrice": {
            "$multiply": [
              { "$arrayElemAt": [
                "$mealitems.price",
                { "$indexOfArray": [ "$mealitems._id", "$$this.meal" ] }
              ]},
              "$$this.quantity"
            ]
          }
        }
      }
    }
  }},
  { "$addFields": {
    "totalOrder": {
      "$sum": "$meals.totalPrice"
    }
  }}
]);

这基本上是由于"mealitems"的结果而产生另一个数组$lookup,然后使用$map来处理原始文档数组并 transpose 返回内容数组的每个项目都重新放入结构中。

您可以结合$arrayElemAt$indexOfArray进行此操作,以在此处找到要转置的匹配项。

使用$multiply的其他计算元素也有一些“算术”,甚至还有使用$addFields的附加$sum阶段来“加总”以给出总体“顺序”总计”。

您“可以”在$project阶段做所有的数学运算(之所以使用它,是因为我们不希望"mealitems"内容。但这涉及更多一点,您可能想使用$let用于数组匹配,因此您不必重复太多代码。

如果您确实愿意,甚至可以使用$lookup的“子管道”形式。代替使用$map来更改返回文档的操作是在返回数组“ 之前内部”完成,在返回结果之前,通过将原始文档数组通过{ {1}}参数:

let

无论哪种形式,这基本上都是// Aggregate with $lookup - sub-pipeline let result2 = await Order.aggregate([ { "$lookup": { "from": Meal.collection.name, "let": { "meals": "$meals" }, "pipeline": [ { "$match": { "$expr": { "$in": [ "$_id", "$$meals.meal" ] } }}, { "$replaceRoot": { "newRoot": { "meal": "$$ROOT", "quantity": { "$arrayElemAt": [ "$$meals.quantity", { "$indexOfArray": [ "$$meals.meal", "$_id" ] } ] }, "totalPrice": { "$multiply": [ { "$arrayElemAt": [ "$$meals.quantity", { "$indexOfArray": [ "$$meals.meal", "$_id" ] } ]}, "$price" ] } } }} ], "as": "meals" }}, { "$addFields": { "totalOrder": { "$sum": "$meals.totalPrice" } }} ]); 通过“合并”内容的幕后寓言,但是当然使用$lookup聚合只是一个请求。

使用populate()

或者,您可以只在JavaScript中操纵结果结构。它已经存在,而您真正需要的只是populate(),以便能够更改生成的对象:

lean()

它看起来很简单,基本上是同一件事,除了已经为您完成了“合并”,当然这是对服务器的两个请求,以便返回所有数据。


作为可复制的完整列表:

// Populate and manipulate
let result3 = await Order.find().populate('meals.meal').lean();

result3 = result3.map(r =>
  ({
      ...r,
      meals: r.meals.map( m =>
        ({
          ...m,
          totalPrice: m.meal.price * m.quantity
        })
      ),
      totalOrder: r.meals.reduce((o, m) =>
        o + (m.meal.price * m.quantity), 0
      )
  })
);

返回的结果如下:

const { Schema } = mongoose = require('mongoose');

// Connection
const uri = 'mongodb://localhost:27017/menu';
const opts = { useNewUrlParser: true };

// Sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);

// Schema defs

const mealSchema = new Schema({
  name: String,
  price: Number
});

const orderSchema = new Schema({
  meals: [
    {
      meal: { type: Schema.Types.ObjectId, ref: 'Meal' },
      quantity: Number
    }
  ]
});

const Meal = mongoose.model('Meal', mealSchema);
const Order = mongoose.model('Order', orderSchema);

// log helper
const log = data => console.log(JSON.stringify(data, undefined, 2));

// main
(async function() {

  try {

    const conn = await mongoose.connect(uri, opts);

    // clean models
    await Promise.all(
      Object.entries(conn.models).map(([k,m]) => m.deleteMany())
    );

    // Set up data

    let [Chicken, Beef] = await Meal.insertMany(
      [
        { name: "Chicken Nuggets", price: 3 },
        { name: "Beef Burger", price: 6 }
      ]
    );

    let order = await Order.create({
      meals: [
        { meal: Chicken, quantity: 12 },
        { meal: Beef, quantity: 4 }
      ]
    });


    // Aggregate with $lookup - traditional
    let result1 = await Order.aggregate([
      { "$lookup": {
        "from": Meal.collection.name,
        "foreignField": "_id",
        "localField": "meals.meal",
        "as": "mealitems"
      }},
      { "$project": {
        "meals": {
          "$map": {
            "input": "$meals",
            "in": {
              "meal": {
                "$arrayElemAt": [
                  "$mealitems",
                  { "$indexOfArray": [ "$mealitems._id", "$$this.meal" ] }
                ]
              },
              "quantity": "$$this.quantity",
              "totalPrice": {
                "$multiply": [
                  { "$arrayElemAt": [
                    "$mealitems.price",
                    { "$indexOfArray": [ "$mealitems._id", "$$this.meal" ] }
                  ]},
                  "$$this.quantity"
                ]
              }
            }
          }
        }
      }},
      { "$addFields": {
        "totalOrder": {
          "$sum": "$meals.totalPrice"
        }
      }}
    ]);
    log(result1);

    // Aggregate with $lookup - sub-pipeline
    let result2 = await Order.aggregate([
      { "$lookup": {
        "from": Meal.collection.name,
        "let": { "meals": "$meals" },
        "pipeline": [
          { "$match": {
            "$expr": {
              "$in": [ "$_id", "$$meals.meal" ]
            }
          }},
          { "$replaceRoot": {
            "newRoot": {
              "meal": "$$ROOT",
              "quantity": {
                "$arrayElemAt": [
                  "$$meals.quantity",
                  { "$indexOfArray": [ "$$meals.meal", "$_id" ] }
                ]
              },
              "totalPrice": {
                "$multiply": [
                  { "$arrayElemAt": [
                    "$$meals.quantity",
                    { "$indexOfArray": [ "$$meals.meal", "$_id" ] }
                  ]},
                  "$price"
                ]
              }
            }
          }}
        ],
        "as": "meals"
      }},
      { "$addFields": {
        "totalOrder": {
          "$sum": "$meals.totalPrice"
        }
      }}
    ]);
    log(result2);

    // Populate and manipulate
    let result3 = await Order.find().populate('meals.meal').lean();

    result3 = result3.map(r =>
      ({
          ...r,
          meals: r.meals.map( m =>
            ({
              ...m,
              totalPrice: m.meal.price * m.quantity
            })
          ),
          totalOrder: r.meals.reduce((o, m) =>
            o + (m.meal.price * m.quantity), 0
          )
      })
    );

    log(result3);

  } catch(e) {
    console.error(e);
  } finally {
    mongoose.disconnect();
  }

})()