$pull object from array, and $pull references in another array

喜你入骨 提交于 2020-01-05 12:16:51

问题


Consider this document from a collection of clients:

client: {
  services: [
    {
      _id: 111,
      someField: 'someVal'
    },
    {
      _id: 222,
      someField: 'someVal'
    }
    ... // More services
  ]

  staff: [
    {
      _id: 'aaa',
      someField: 'someVal',
      servicesProvided: [111, 222, 333, ...]
    },
    {
      _id: 'bbb',
      someField: 'someVal',
      servicesProvided: [111, 555, 666, ...]
    },
    {
      _id: 'ccc',
      someField: 'someVal',
      servicesProvided: [111, 888, 999, ...]
    }
    ... // More staff
  ]
}

A client can have many staff members. Each staff has a reference to the services he or she provide. If a service is deleted, reference to this service also need to be deleted in all staff.

I want to delete (pull) an object (a service) from services, and in the same query delete possible reference in the servicesProvided in all staff objects`

For example, if I delete service with _id 111, I also want to delete all references to this service in staffmembers that provide this service.

How do i write this query.


回答1:


So this is where things get a little nasty. How indeed do you update "multiple" array items that would match the conditions in a single document?

A bit of background here comes from the positional $ operator documentation:

Nested Arrays The positional $ operator cannot be used for queries which traverse more than one array, such as queries that traverse arrays nested within other arrays, because the replacement for the $ placeholder is a single value

That tells "part" of the story, but the main point here that is specific to this question is "more that one".

So even though the "nested" part is not explicitly true due to what needs to be done, the important factor is "more than one". To demonstrate, lets consider this:

{
  services: [
    {
      _id: 111,
      someField: 'someVal'
    },
    {
      _id: 222,
      someField: 'someVal'
    }
  ],

  staff: [
    {
      _id: 'aaa',
      someField: 'someVal',
      servicesProvided: [111, 222, 333, ...]
    },
    {
      _id: 'bbb',
      someField: 'someVal',
      servicesProvided: [111, 555, 666, ...]
    },
    {
      _id: 'ccc',
      someField: 'someVal',
      servicesProvided: [111, 888, 999, ...]
    }
  ]
}

Now you ask to remove the 111 value. This is always the "first" value as provided in your example. So where we can assume this to be the case then the update is "what seems to be: simple:

 db.collection.update(
     { 
         "_id": ObjectId("542ea4991cf4ad425615b84f"),
     },
     { 
         "$pull": {
             "services": { "_id": 111 },
             "staff.servicesProvided": 111
         }
     }
 )

But. That won't do what you expect as the elements will not be pulled from all "staff" array elements as you might expect. In fact, none of them. The only thing that will work is this:

 db.collection.update(
     { 
         "_id": ObjectId("542ea4991cf4ad425615b84f"),
         "staff.servicesProvided": 111
     },
     { 
         "$pull": {
             "services": { "_id": 111 },
             "staff.$.servicesProvided": 111
         }
     }
 )

But guess what! Only the "first" array element was actually updated. So when you look at the statement above, this is basically what it says will happen.

Then again though, suppose we were just testing this in a modern MongoDB shell with a server of MongoDB 2.6 version or greater. Then this is the response we get:

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

So hang on a moment. We were just told how many documents were "modified" by the last statement. So even though we can only change one element of the array at a time, there is some important feedback here to the condition.

The really great thing about the new "WriteResult" objects obtained from "Bulk Operations API" operations, which in fact this is doing in the shell, is that you actually get told if something was "modified" by the previous statement or not. Way better than the "legacy" write responses, this now gives us a grounding to make some important decisions on looping considerations. Such as "Did our last operation actually 'modify' a document, and then should we continue?"

So this is an important "flow control" point, even if the general MongoDB API itself cannot just "update all elements" all at once. Now there is a testable case to decide where to "continue" in a loop or not. This is what I mean finally by "combining" what you have already learned. So eventually we can come to a listing like this:

var bulk = db.collection.initializeOrderedBulkOp();
var modified = 1;

async.whilst(
    function() { return modified },
    function(callback) {
        bulk.find(
            { 
                "_id": ObjectId("542ea4991cf4ad425615b84f"),
                "staff.servicesProvided": 111
            }
        ).updateOne(
            { 
                "$pull": {
                     "services": { "_id": 111 },
                     "staff.$.servicesProvided": 111
                }
            }
        );

        bulk.execute(function(err,result) {
            modified = result.nModfified();
            callback(err);
        });
    },
    function(err) {
      // did I throw something! Suppose I should so something about it!
    }
);

Or basically something cute like that. So you are asking for the "result" object obtained from the "bulk operations" .execute() to tell you if something was modified or not. Where it still was, then you are "re-iterating" the loop again here and performing the same update and asking for the result again.

Eventually, the update operation will tell you that "nothing" was modified at all. This is when you exit the loop and continue normal operations.

Now an alternate way to handle this might well be to read in the entire object and then make all the modifications that you require:

db.collection.findOne(
    { 
        "_id": ObjectId("542ea4991cf4ad425615b84f"),
        "staff.servicesProvided": 111
    },
    function(err,doc) {
        doc.services = doc.services.filter(function(item) {
            return item._id != 111;
        });

        doc.staff = doc.staff.filter(function(item) {
            item.serviceProvided = item.servicesProvided.filter(function(sub) {
                return sub != 111;
            });
            return item;
        });
       db.collection.save( doc );
    }
);

Bit of overkill. Not entirely atomic, but close enough for measure.

So you cannot really do this in a single write operation, at least without dealing with "reading" the document and then "writing" the whole thing back after modifying the content. But you can take and "iterative" approach, and there are the tools around to allow you to control that.

Another possible way to approach this is to change the way you model like this:

{
  "services": [
    {
      "_id": 111,
      "someField": "someVal"
    },
    {
      "_id": 222,
      "someField": "someVal"
    }
  ],

  "provided": [ 
      { "_id": "aaa", "service": 111 },
      { "_id": "aaa", "service": 222 },
      { "_id": "aaa", "service": 111 }
  ]
}

And so on. So then the query becomes something like this:

db.collection.update(
    {  "_id": ObjectId("542ea4991cf4ad425615b84f") },
    {
        "$pull": {
            "services": { "_id": 111 },
            "provided": { "_id": 111 }
        }
    }
);

And that truly would be a singular update operation that removes everything in one go because each element is contained in singular arrays.

So there are ways to do it, but how you model really depends on your application data access patterns. Choose the solution that suits you best. This is why you choose MongoDB in the first place.



来源:https://stackoverflow.com/questions/26179021/pull-object-from-array-and-pull-references-in-another-array

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