Can I access the positional $ operator in projection of findOneAndUpdate

孤街醉人 提交于 2021-01-27 15:31:51

问题


I have this query that works, but I want for the doc to only display network.stations.$ instead of the entire array. If I write fields: network.stations.$, I get an error. Is there a way for the doc only to return a single element from [stations]?

Network.findOneAndUpdate({
  "network.stations.id": req.params.station_Id
}, {
  "network.stations.$.free_bikes": req.body.free_bikes
}, {
  new: true,
  fields: "network.stations"
}, (err, doc) => console.log(doc)) 
// I want doc to somehow point only to a single station instead of 
// several stations like it currently does.

回答1:


The answer is "yes", but not in the way you are expecting. As you note in the question, putting network.stations.$ in the "fields" option to positionally return the "modified" document throws a specific error:

"cannot use a positional projection and return the new document"

This however should be the "hint", because you don't really "need" the "new document" when you know what the value was you are modifying. The simple case then is to not return the "new" document, but instead return it's "found state" which was "before the atomic modification" and simply make the same modification to the returned data as you asked to apply in the statement.

As a small contained demo:

const mongoose = require('mongoose'),
      Schema = mongoose.Schema;

mongoose.Promise = global.Promise;
mongoose.set('debug',true);

const uri = 'mongodb://localhost/test',
      options = { useMongoClient: true };

const testSchema = new Schema({},{ strict: false });

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

function log(data) {
  console.log(JSON.stringify(data,undefined,2))
}

(async function() {

  try {

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

    await Test.remove();

    await Test.insertMany([{ a: [{ b: 1 }, { b: 2 }] }]);

    for ( let i of [1,2] ) {
      let result = await Test.findOneAndUpdate(
        { "a.b": { "$gte": 2 } },
        { "$inc": { "a.$.b": 1 } },
        { "fields": { "a.$": 1 } }
      ).lean();

      console.log('returned');
      log(result);

      result.a[0].b = result.a[0].b + 1;
      console.log('modified');
      log(result);

    }

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


})();

Which produces:

Mongoose: collection.remove({}, {})
Mongoose: collection.insertMany([ { __v: 0, a: [ { b: 1 }, { b: 2 } ], _id: 59af214b6fb3533d274928c9 } ])
Mongoose: collection.findAndModify({ 'a.b': { '$gte': 2 } }, [], { '$inc': { 'a.$.b': 1 } }, { new: false, upsert: false, fields: { 'a.$': 1 } })
returned
{
  "_id": "59af214b6fb3533d274928c9",
  "a": [
    {
      "b": 2
    }
  ]
}
modified
{
  "_id": "59af214b6fb3533d274928c9",
  "a": [
    {
      "b": 3
    }
  ]
}
Mongoose: collection.findAndModify({ 'a.b': { '$gte': 2 } }, [], { '$inc': { 'a.$.b': 1 } }, { new: false, upsert: false, fields: { 'a.$': 1 } })
returned
{
  "_id": "59af214b6fb3533d274928c9",
  "a": [
    {
      "b": 3
    }
  ]
}
modified
{
  "_id": "59af214b6fb3533d274928c9",
  "a": [
    {
      "b": 4
    }
  ]
}

So I'm doing the modifications in a loop so you can see that the update is actually applied on the server as the next iteration increments the already incremented value.

Merely by omitting the "new" option, what you get is the document in the state which it was "matched" and it then is perfectly valid to return that document state before modification. The modification still happens.

All you need to do here is in turn make the same modification in code. Adding .lean() makes this simple, and again it's perfectly valid since you "know what you asked the server to do".

This is better than a separate query because "separately" the document can be modified by a different update in between your modification and the query to return just a projected matched field.

And it's better than returning "all" the elements and filtering later, because the potential could be a "very large array" when all you really want is the "matched element". Which of course this actually does.




回答2:


Try changing fields to projection and then use the network.stations.$ like you tried before.

If your query is otherwise working then that might be enough. If it's still not working you can try changing the second argument to explicitly $set.

Network.findOneAndUpdate({
  "network.stations.id": req.params.station_Id
}, {
  "$set": {
    "network.stations.$.free_bikes": req.body.free_bikes
  }
}, {
  new: true,
  projection: "network.stations.$"
}, (err, doc) => console.log(doc))


来源:https://stackoverflow.com/questions/46063660/can-i-access-the-positional-operator-in-projection-of-findoneandupdate

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!