Find first element in nested array that match conditions

那年仲夏 提交于 2019-12-07 11:36:45

问题


I have following documents:

doc1: { 
  'array': [
    {'field': 'ABC', 'enabled': 'false'},
    {'field': 'BCD', 'enabled': 'true'},
    {'field': 'DEF', 'enabled': 'false'},
    {'field': 'XYZ', 'enabled': 'true'},
  ]
}


doc2: { 
  'array': [
    {'field': 'ABC', 'enabled': 'true'},
    {'field': 'BCD', 'enabled': 'true'},
    {'field': 'DEF', 'enabled': 'false'},
    {'field': 'XYZ', 'enabled': 'true'},
  ]
}

I'm performing search by particular field. I want to get all documents that have this field enabled, and there are no other fields enabled before this (in the list).

For ex.:

search for field: BCD, enabled: true - should return only first document (because in second document ABC is enabled too).

search for XYZ, enabled: true - should not return any documents at all, because there are other fields enabled on top of this list.

I was trying to do smth with $elemMatch, but I don't know if it's possible to apply filter on top of element that elemMatch has matched.

Any suggestions?


回答1:


Best probably to search using $where in addition to the normal query, and still keep things on the server:

db.getCollection('collection').find({
  "array": {
    "$elemMatch": { "field": "BCD", "enabled": "true" },
  },
  "$where": function() {
    return this.array.map((e,i) => Object.assign(e,{ i }))
      .filter( e => e.field === "BCD" && e.enabled === "true" )
      .map( e => e.i )[0] <=
    this.array.map(e => e.enabled).indexOf("true")
  }  
})

And if you have MongoDB 3.4 with support for $indexOfArray and $range, then it may look longer but it actually most efficient with $redact:

db.getCollection('collection').aggregate([
  { "$match": {
    "array": {
      "$elemMatch": { "field": "BCD", "enabled": "true" },
    }
  }},
  { "$redact": {
    "$cond": {  
      "if": {
        "$lte": [
          { "$arrayElemAt": [
            { "$map": {
              "input": {
                "$filter": {
                  "input": {
                    "$map": {
                      "input": {
                        "$zip": {
                          "inputs": [
                            "$array",
                            { "$range": [0, { "$size": "$array" }] }
                          ]
                        }    
                      },
                      "as": "a",
                      "in": {
                        "field": { "$arrayElemAt": [ "$$a.field", 0 ] },
                        "enabled": { "$arrayElemAt": [ "$$a.enabled", 0 ] },
                        "index": { "$arrayElemAt": [ "$$a", 1 ] }    
                      }
                    }
                  },
                  "as": "a",
                  "cond": {
                    "$and": [
                      { "$eq": [ "$$a.field", "BCD" ] },
                      { "$eq": [ "$$a.enabled", "true" ] }
                    ]
                  }    
                }
              },
              "as": "a",
              "in": "$$a.index"  
            }},
            0
          ]},
          { "$indexOfArray": [ "$array.enabled", "true" ] } 
        ] 
      },
      "then": "$$KEEP",
      "else": "$$PRUNE"
    }
  }}
])

So there really is no actual query operation that enforces that, but both of these cases keep the selection "on the server" as opposed to sending data over the wire to the client and then filtering.

Because if you do that it kind of negates the purpose of using a database in the first place. So you really want this to happen on the server.




回答2:


Why not search by enabled field and then check if field is appropriate or not?

db.collection("col").findOne({
    "array.enabled": true
}, {
    array: {
        $elemMatch {
            enabled: true
        }
    }
})
.then(function(docs){
    docs.forEach(function(doc){
        if(doc.array[0].field == "ABC"){
            // Ok, we get it
        }
    })
})

Second argument to find is projection, so application doesn't download complete array from document, but only first matched element of it.




回答3:


There is $where keyword that allow to generate complex conditions for similar purpose.

find($where: 'this.array.filter(function(e){return e.enabled=="true"})[0].field=="ABC"')

As this does not use any indexes, I'll add more conditions to benefit from optimisation.



来源:https://stackoverflow.com/questions/45736687/find-first-element-in-nested-array-that-match-conditions

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