首页 > 解决方案 > 每次获取文档时都将基于其他字段的新字段添加到文档中

问题描述

我的问题架构如下所示:

const questionSchema = new mongoose.Schema({
    content: String,
    options: [{
        content: String,
        correct: Boolean
    }]
});

我也有一个测试模式,我指的是问题:

const testSchema = new mongoose.Schema({
    // ... 
    questions: [{
        type: mongoose.Schema.Types.ObjectId,
        ref: "Question"
    }]
})

当我获取问题(使用或)时find(),我想在文档中添加一个基于有多少的新布尔字段。预期输出:findOne()Test.find().populate("questions")multipleoptionscorrect === true

{
    _id: "...",
    _v: 1,
    content: "What is a capital of Slovenia?"
    options: [
        {
            content: "Bled",
            correct: false
        },
        {
            content: "Ljubljana",
            correct: true
        }
    ],
    multiple: false
}

是否可以使用某种在我每次查询问题时调用的函数并将新字段添加到获取的对象中,或者我是否必须将multiple字段永久存储在 Mongo 中?

标签: javascriptmongodbmongoose

解决方案


根据您的需要,这里有几种方法。

猫鼬虚拟场

最直接的应该是因为您使用的是 mongoose 将向virtual模式添加一个字段,该字段基本上在访问时计算它的值。您没有在问题中指定您的逻辑,但假设“多个”之类的内容true意味着您将执行以下操作:multipletrue

const questionSchema = new Schema({
  content: String,
  options: [{
    content: String,
    correct: Boolean
  }]
},
{
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

questionSchema.virtual('multiple').get(function() {
  return this.options.filter(e => e.correct).length > 1;
});

这是一个基本的“getter”,它只是查看数组内容并在数组内容true中属性的元素数量correct多于一个时返回。它实际上可以是函数中您想要的任何逻辑。注意function()and not的使用,() =>因为“箭头函数”具有不同的范围,this并且对于 mongoose 在评估时确定当前实例值很重要。

架构定义中的toJSON和选项是可选的,但基本上它们的要点是您可以直接访问“虚拟”属性(即),但除非该定义与这些选项一起添加,否则类似的东西不会显示属性。toObjectquestion.multiple === falseconsole.log(question)virtual

MongoDB 投影

另一种选择是让 MongoDB 在结果中从服务器返回修改后的文档。这是使用聚合框架完成的,它基本上是任何“结果操作”的工具。

这里作为一个例子,我们实现了与虚拟方法中相同的逻辑,并以$lookup相同的方式使用 a populate()。当然,这是对服务器的一个请求,而不是像 的情况那样的两个populate()请求,它只是对“相关”数据发出单独的查询:

// Logic in aggregate result
let result = await Test.aggregate([
  { "$lookup": {
    "from": Question.collection.name,
    "let": { "questions": "$questions" },
    "pipeline": [
      { "$match": {
        "$expr": {
          "$in": [ "$_id", "$$questions" ]
        }
      }},
      { "$addFields": {
        "multiple": {
          "$gt": [
            { "$size": {
              "$filter": {
                "input": "$options",
                "cond": "$$this.correct"
              }
            }},
            1
          ]
        }
      }}
    ],
    "as": "questions"
  }}
]);

$filter与代替Array.filter()$size代替的相同类型的操作Array.length。同样,主要的好处是这里的“服务器连接”,因此您最好在那里实现“虚拟”逻辑而不是在模式上实现。

虽然“可能”使用aggregate()带有 mongoose 模式和方法的结果,但默认行为是aggregate()返回“普通对象”而不是具有模式方法的“mongoose 文档”实例。您可以重新转换结果并使用模式方法,但这可能意味着仅为特定的“聚合”结果定义“特殊”模式和模型类,这可能不是最有效的做法。


总体而言,您实现哪一个取决于哪一个最适合您的应用程序需求。

当然,虽然您“可以”也只是将相同的数据存储在 MongoDB 文档中,而不是每次检索它时都进行计算,但开销基本上转移到了写入数据的时间,这主要取决于您如何写入数据。例如,如果您向现有选项“添加新选项”,那么您基本上需要从 MongoDB 读取整个文档,检查内容,然后决定为该multiple值写回什么。因此,此处呈现的相同逻辑(true数组中的多个)没有“原子”写入过程,无需先读取文档数据即可完成。

作为这些方法的一个工作示例,请参见以下清单:

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

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

mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);

const questionSchema = new Schema({
  content: String,
  options: [{
    content: String,
    correct: Boolean
  }]
},
{
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

questionSchema.virtual('multiple').get(function() {
  return this.options.filter(e => e.correct).length > 1;
});

const testSchema = new Schema({
  questions: [{
    type: Schema.Types.ObjectId,
    ref: 'Question'
  }]
});


const Question = mongoose.model('Question', questionSchema);
const Test = mongoose.model('Test', testSchema);

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


(async function() {

  try {

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

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

    // Insert some data

    let questions = await Question.insertMany([
      {
        "content": "What is the a capital of Slovenia?",
        "options": [
          { "content": "Bled", "correct": false },
          { "content": "Ljubljana", "correct": true }
        ]
      },
      {
        "content": "Who are the most excellent people?",
        "options": [
          { "content": "Bill", "correct": true },
          { "content": "Ted", "correct": true },
          { "content": "Evil Duke", "correct": false }
        ]
      }
    ]);

    await Test.create({ questions })


    // Just the questions
    let qresults = await Question.find();
    log(qresults);

    // Test with populated questions
    let test = await Test.findOne().populate('questions');
    log(test);

    // Logic in aggregate result
    let result = await Test.aggregate([
      { "$lookup": {
        "from": Question.collection.name,
        "let": { "questions": "$questions" },
        "pipeline": [
          { "$match": {
            "$expr": {
              "$in": [ "$_id", "$$questions" ]
            }
          }},
          { "$addFields": {
            "multiple": {
              "$gt": [
                { "$size": {
                  "$filter": {
                    "input": "$options",
                    "cond": "$$this.correct"
                  }
                }},
                1
              ]
            }
          }}
        ],
        "as": "questions"
      }}
    ]);

    log(result);


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

})()

它的输出:

Mongoose: questions.deleteMany({}, {})
Mongoose: tests.deleteMany({}, {})
Mongoose: questions.insertMany([ { _id: 5cce2f0b83d75c2d1fe6f728, content: 'What is the a capital of Slovenia?', options: [ { _id: 5cce2f0b83d75c2d1fe6f72a, content: 'Bled', correct: false }, { _id: 5cce2f0b83d75c2d1fe6f729, content: 'Ljubljana', correct: true } ], __v: 0 }, { _id: 5cce2f0b83d75c2d1fe6f72b, content: 'Who are the most excellent people?', options: [ { _id: 5cce2f0b83d75c2d1fe6f72e, content: 'Bill', correct: true }, { _id: 5cce2f0b83d75c2d1fe6f72d, content: 'Ted', correct: true }, { _id: 5cce2f0b83d75c2d1fe6f72c, content: 'Evil Duke', correct: false } ], __v: 0 } ], {})
Mongoose: tests.insertOne({ questions: [ ObjectId("5cce2f0b83d75c2d1fe6f728"), ObjectId("5cce2f0b83d75c2d1fe6f72b") ], _id: ObjectId("5cce2f0b83d75c2d1fe6f72f"), __v: 0 })
Mongoose: questions.find({}, { projection: {} })
[
  {
    "_id": "5cce2f0b83d75c2d1fe6f728",
    "content": "What is the a capital of Slovenia?",
    "options": [
      {
        "_id": "5cce2f0b83d75c2d1fe6f72a",
        "content": "Bled",
        "correct": false
      },
      {
        "_id": "5cce2f0b83d75c2d1fe6f729",
        "content": "Ljubljana",
        "correct": true
      }
    ],
    "__v": 0,
    "multiple": false,
    "id": "5cce2f0b83d75c2d1fe6f728"
  },
  {
    "_id": "5cce2f0b83d75c2d1fe6f72b",
    "content": "Who are the most excellent people?",
    "options": [
      {
        "_id": "5cce2f0b83d75c2d1fe6f72e",
        "content": "Bill",
        "correct": true
      },
      {
        "_id": "5cce2f0b83d75c2d1fe6f72d",
        "content": "Ted",
        "correct": true
      },
      {
        "_id": "5cce2f0b83d75c2d1fe6f72c",
        "content": "Evil Duke",
        "correct": false
      }
    ],
    "__v": 0,
    "multiple": true,
    "id": "5cce2f0b83d75c2d1fe6f72b"
  }
]
Mongoose: tests.findOne({}, { projection: {} })
Mongoose: questions.find({ _id: { '$in': [ ObjectId("5cce2f0b83d75c2d1fe6f728"), ObjectId("5cce2f0b83d75c2d1fe6f72b") ] } }, { projection: {} })
{
  "questions": [
    {
      "_id": "5cce2f0b83d75c2d1fe6f728",
      "content": "What is the a capital of Slovenia?",
      "options": [
        {
          "_id": "5cce2f0b83d75c2d1fe6f72a",
          "content": "Bled",
          "correct": false
        },
        {
          "_id": "5cce2f0b83d75c2d1fe6f729",
          "content": "Ljubljana",
          "correct": true
        }
      ],
      "__v": 0,
      "multiple": false,
      "id": "5cce2f0b83d75c2d1fe6f728"
    },
    {
      "_id": "5cce2f0b83d75c2d1fe6f72b",
      "content": "Who are the most excellent people?",
      "options": [
        {
          "_id": "5cce2f0b83d75c2d1fe6f72e",
          "content": "Bill",
          "correct": true
        },
        {
          "_id": "5cce2f0b83d75c2d1fe6f72d",
          "content": "Ted",
          "correct": true
        },
        {
          "_id": "5cce2f0b83d75c2d1fe6f72c",
          "content": "Evil Duke",
          "correct": false
        }
      ],
      "__v": 0,
      "multiple": true,
      "id": "5cce2f0b83d75c2d1fe6f72b"
    }
  ],
  "_id": "5cce2f0b83d75c2d1fe6f72f",
  "__v": 0
}
Mongoose: tests.aggregate([ { '$lookup': { from: 'questions', let: { questions: '$questions' }, pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$questions' ] } } }, { '$addFields': { multiple: { '$gt': [ { '$size': { '$filter': { input: '$options', cond: '$$this.correct' } } }, 1 ] } } } ], as: 'questions' } } ], {})
[
  {
    "_id": "5cce2f0b83d75c2d1fe6f72f",
    "questions": [
      {
        "_id": "5cce2f0b83d75c2d1fe6f728",
        "content": "What is the a capital of Slovenia?",
        "options": [
          {
            "_id": "5cce2f0b83d75c2d1fe6f72a",
            "content": "Bled",
            "correct": false
          },
          {
            "_id": "5cce2f0b83d75c2d1fe6f729",
            "content": "Ljubljana",
            "correct": true
          }
        ],
        "__v": 0,
        "multiple": false
      },
      {
        "_id": "5cce2f0b83d75c2d1fe6f72b",
        "content": "Who are the most excellent people?",
        "options": [
          {
            "_id": "5cce2f0b83d75c2d1fe6f72e",
            "content": "Bill",
            "correct": true
          },
          {
            "_id": "5cce2f0b83d75c2d1fe6f72d",
            "content": "Ted",
            "correct": true
          },
          {
            "_id": "5cce2f0b83d75c2d1fe6f72c",
            "content": "Evil Duke",
            "correct": false
          }
        ],
        "__v": 0,
        "multiple": true
      }
    ],
    "__v": 0
  }
]

推荐阅读