web-dev-qa-db-fra.com

MongoDB: sous-document upsert

J'ai des documents qui ressemblent à ça, avec un index unique sur bars.name:

{ name: 'foo', bars: [ { name: 'qux', somefield: 1 } ] }

. Je souhaite mettre à jour le sous-document où { name: 'foo', 'bars.name': 'qux' } et $set: { 'bars.$.somefield': 2 }, ou créez un nouveau sous-document avec { name: 'qux', somefield: 2 } en dessous de { name: 'foo' }.

Est-il possible de le faire en utilisant une seule requête avec upsert, ou devrai-je en émettre deux?

Connexes: 'upsert' dans un document incorporé (suggère de changer le schéma pour avoir l'identifiant de sous-document comme clé, mais cela date d'il y a deux ans et je me demande s'il y a de meilleures solutions maintenant.)

32
shesek

Non, il n'y a pas vraiment de meilleure solution à cela, alors peut-être avec une explication.

Supposons que vous ayez un document en place qui a la structure que vous montrez:

{ 
  "name": "foo", 
  "bars": [{ 
       "name": "qux", 
       "somefield": 1 
  }] 
}

Si vous faites une mise à jour comme celle-ci

db.foo.update(
    { "name": "foo", "bars.name": "qux" },
    { "$set": { "bars.$.somefield": 2 } },
    { "upsert": true }
)

Alors tout va bien car le document correspondant a été trouvé. Mais si vous modifiez la valeur de "bars.name":

db.foo.update(
    { "name": "foo", "bars.name": "xyz" },
    { "$set": { "bars.$.somefield": 2 } },
    { "upsert": true }
)

Ensuite, vous obtiendrez un échec. La seule chose qui a vraiment changé ici, c'est que dans MongoDB 2.6 et au-dessus, l'erreur est un peu plus succincte:

WriteResult({
    "nMatched" : 0,
    "nUpserted" : 0,
    "nModified" : 0,
    "writeError" : {
        "code" : 16836,
        "errmsg" : "The positional operator did not find the match needed from the query. Unexpanded update: bars.$.somefield"
    }
})

C'est mieux à certains égards, mais vous ne voulez vraiment pas "bouleverser" de toute façon. Ce que vous voulez faire, c'est ajouter l'élément au tableau où le "nom" n'existe pas actuellement.

Donc, ce que vous voulez vraiment, c'est le "résultat" de la tentative de mise à jour sans l'indicateur "upsert" pour voir si des documents ont été affectés:

db.foo.update(
    { "name": "foo", "bars.name": "xyz" },
    { "$set": { "bars.$.somefield": 2 } }
)

Rendement en réponse:

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

Ainsi, lorsque les documents modifiés sont 0 vous savez que vous souhaitez publier la mise à jour suivante:

db.foo.update(
    { "name": "foo" },
    { "$Push": { "bars": {
        "name": "xyz",
        "somefield": 2
    }}
)

Il n'y a vraiment pas d'autre moyen de faire exactement ce que vous voulez. Comme les ajouts au tableau ne sont pas strictement un type d'opération "set", vous ne pouvez pas utiliser $addToSet combiné avec la fonctionnalité "mise à jour groupée" , afin que vous puissiez "cascader" vos demandes de mise à jour.

Dans ce cas, il semble que vous deviez vérifier le résultat, ou accepter la lecture de tout le document et vérifier s'il faut mettre à jour ou insérer un nouvel élément de tableau dans le code.

43
Neil Lunn

si cela ne vous dérange pas de changer un peu le schéma et d'avoir une structure comme ça:

{ "name": "foo", "bars": { "qux": { "somefield": 1 },
                           "xyz": { "somefield": 2 },
                  }
}

Vous pouvez effectuer vos opérations en une seule fois. Réitération 'upsert' dans un document incorporé pour être complet

3
nesdis

Il n'y a aucun moyen de le faire si la mise à jour repose sur une référence à l'enregistrement en cours de mise à jour (par exemple, mise à jour x => x + 1). L'émission de 2 commandes distinctes (définir puis insérer) entraîne une condition de concurrence critique qui ne peut pas être résolue en vérifiant les doublons, car si votre insertion est rejetée en raison d'un doublon, vous perdriez l'effet de votre mise à jour (par exemple, x ne le ferait pas être incrémenté correctement dans l'exemple ci-dessus). Ce serait bien si MongoDB ajoutait cette fonctionnalité, car la possibilité d'implémenter des documents intégrés est un gros atout pour MongoDB pour certaines applications.

1
bluenike

Il existe un moyen de le faire dans deux requêtes - mais cela fonctionnera toujours dans un bulkWrite.

Ceci est pertinent car dans mon cas, le fait de ne pas pouvoir créer de lots est le plus gros blocage. Avec cette solution, vous n'avez pas besoin de collecter le résultat de la première requête, ce qui vous permet d'effectuer des opérations en bloc si vous en avez besoin.

Voici les deux requêtes successives à exécuter pour votre exemple:

// Update subdocument if existing
collection.updateMany({
    name: 'foo', 'bars.name': 'qux' 
}, {
    $set: { 
        'bars.$.somefield': 2 
    }
})
// Insert subdocument otherwise
collection.updateMany({
    name: 'foo', $not: {'bars.name': 'qux' }
}, {
    $Push: { 
        bars: {
            somefield: 2, name: 'qux'
        }
    }
})

Cela présente également l'avantage supplémentaire de ne pas avoir de données/conditions de concurrence corrompues si plusieurs applications écrivent simultanément dans la base de données. Vous ne risquez pas de vous retrouver avec deux bars: {somefield: 2, name: 'qux'} sous-documents dans votre document si deux applications exécutent les mêmes requêtes en même temps.

0
coyotte508