web-dev-qa-db-fra.com

Projection MongoDB de tableaux imbriqués

J'ai une collection "comptes" qui contient des documents similaires à cette structure:

{
    "email" : "[email protected]",
    "groups" : [
        {
            "name" : "group1",
            "contacts" : [
                { "localId" : "c1", "address" : "some address 1" },
                { "localId" : "c2", "address" : "some address 2" },
                { "localId" : "c3", "address" : "some address 3" }
            ]
        },
        {
            "name" : "group2",
            "contacts" : [
                { "localId" : "c1", "address" : "some address 1" },
                { "localId" : "c3", "address" : "some address 3" }
            ]
        }
    ]
}

Via

q = { "email" : "[email protected]", "groups" : { $elemMatch: { "name" : "group1" } } }
p = { "groups.name" : 0, "groups" : { $elemMatch: { "name" : "group1" } } }
db.accounts.find( q, p ).pretty()

J'obtiendrai avec succès le groupe d'un compte spécifié qui m'intéresse.

Question: Comment puis-je obtenir une liste limitée de "contacts" au sein d'un certain "groupe" d'un "compte" spécifié? Supposons que j'ai les arguments suivants:

  • compte: email - "[email protected]"
  • groupe: nom - "groupe1"
  • contact: tableau de localIds - ["c1", "c3", "ID non existant"]

Compte tenu de ces arguments, j'aimerais avoir le résultat suivant:

{
    "groups" : [
        {
            "name" : "group1", (might be omitted)
            "contacts" : [
                { "localId" : "c1", "address" : "some address 1" },
                { "localId" : "c3", "address" : "some address 3" }
            ]
        }
    ]
}

Je n'ai besoin de rien d'autre que des contacts résultants.

Approches

Toutes les requêtes tentent de récupérer un seul contact correspondant au lieu d'une liste de contacts correspondants, par souci de simplicité. J'ai essayé les requêtes suivantes sans succès:

p = { "groups.name" : 0, "groups" : { $elemMatch: { "name" : "group1", "contacts" : { $elemMatch: { "localId" : "c1" } } } } }
p = { "groups.name" : 0, "groups" : { $elemMatch: { "name" : "group1", "contacts.localId" : "c1" } } }
not working: returns whole array or nothing depending on localId


p = { "groups.$" : { $elemMatch: { "localId" : "c1" } } }
error: {
    "$err" : "Can't canonicalize query: BadValue Cannot use $elemMatch projection on a nested field.",
    "code" : 17287
}


p = { "groups.contacts" : { $elemMatch: { "localId" : "c1" } } }
error: {
    "$err" : "Can't canonicalize query: BadValue Cannot use $elemMatch projection on a nested field.",
    "code" : 17287
}

Toute aide est appréciée!

28
cbopp

Mise à jour 2017

Une question aussi bien posée mérite une réponse moderne. Le type de filtrage de tableau demandé peut en fait être effectué dans les versions modernes de MongoDB post 3.2 via simplement $match et $project les étapes du pipeline, un peu comme l'intention de l'opération de requête simple d'origine.

db.accounts.aggregate([
  { "$match": {
    "email" : "[email protected]",
    "groups": {
      "$elemMatch": { 
        "name": "group1",
        "contacts.localId": { "$in": [ "c1","c3", null ] }
      }
    }
  }},
  { "$addFields": {
    "groups": {
      "$filter": {
        "input": {
          "$map": {
            "input": "$groups",
            "as": "g",
            "in": {
              "name": "$$g.name",
              "contacts": {
                "$filter": {
                  "input": "$$g.contacts",
                  "as": "c",
                  "cond": {
                    "$or": [
                      { "$eq": [ "$$c.localId", "c1" ] },
                      { "$eq": [ "$$c.localId", "c3" ] }
                    ]
                  } 
                }
              }
            }
          }
        },
        "as": "g",
        "cond": {
          "$and": [
            { "$eq": [ "$$g.name", "group1" ] },
            { "$gt": [ { "$size": "$$g.contacts" }, 0 ] }
          ]
        }
      }
    }
  }}
])

Cela utilise les opérateurs $filter et $map pour ne renvoyer les éléments des tableaux que si les conditions le remplissaient. beaucoup mieux pour les performances que d'utiliser $unwind . Étant donné que les étapes du pipeline reflètent effectivement la structure de la "requête" et du "projet" à partir d'une opération .find(), les performances ici sont essentiellement comparables à celles-ci et à l'opération.

Notez que lorsque l'intention est de travailler réellement "entre les documents" pour rassembler les détails à partir de "plusieurs" documents plutôt que "un", alors cela nécessiterait généralement un certain type de $unwind pour ce faire, permettant ainsi aux éléments du tableau d'être accessibles pour le "regroupement".


C'est fondamentalement l'approche:

db.accounts.aggregate([
    // Match the documents by query
    { "$match": {
        "email" : "[email protected]",
        "groups.name": "group1",
        "groups.contacts.localId": { "$in": [ "c1","c3", null ] },
    }},

    // De-normalize nested array
    { "$unwind": "$groups" },
    { "$unwind": "$groups.contacts" },

    // Filter the actual array elements as desired
    { "$match": {
        "groups.name": "group1",
        "groups.contacts.localId": { "$in": [ "c1","c3", null ] },
    }},

    // Group the intermediate result.
    { "$group": {
        "_id": { "email": "$email", "name": "$groups.name" },
        "contacts": { "$Push": "$groups.contacts" }
    }},

    // Group the final result
    { "$group": {
        "_id": "$_id.email",
        "groups": { "$Push": {
            "name": "$_id.name",
            "contacts": "$contacts" 
        }}
    }}
])

Il s'agit d'un "filtrage de tableaux" sur plusieurs correspondances, ce que les capacités de projection de base de .find() ne peuvent pas faire.

Vous avez des tableaux "imbriqués", vous devez donc traiter deux fois $unwind . Avec les autres opérations.

34
Neil Lunn

Vous pouvez utiliser l'opérateur $ unwind du framework d'agrégation. Par exemple:

db.contact.aggregate({$unwind:'$groups'}, {$unwind:'$groups.contacts'}, {$match:{email:'[email protected]', 'groups.name':'group1', 'groups.contacts.localId':{$in:['c1', 'c3', 'whatever']}}});

Devrait donner le résultat suivant:

{ "_id" : ObjectId("5500103e706342bc096e2e14"), "email" : "[email protected]", "groups" : { "name" : "group1", "contacts" : { "localId" : "c1", "address" : "some address 1" } } }
{ "_id" : ObjectId("5500103e706342bc096e2e14"), "email" : "[email protected]", "groups" : { "name" : "group1", "contacts" : { "localId" : "c3", "address" : "some address 3" } } }

Si vous ne voulez qu'un seul objet, vous pouvez alors utiliser l'opérateur $ group .

3
Niabb