183 votes

mongodb : insert if not exists

Chaque jour, je reçois un stock de documents (une mise à jour). Ce que je veux faire, c'est insérer chaque élément qui n'existe pas déjà.

  • Je veux aussi garder la trace de la première fois où je les ai insérés, et de la dernière fois où je les ai vus dans une mise à jour.
  • Je ne veux pas avoir de documents en double.
  • Je ne veux pas supprimer un document qui a déjà été enregistré, mais qui ne figure pas dans ma mise à jour.
  • 95 % (estimation) des enregistrements ne sont pas modifiés d'un jour à l'autre.

J'utilise le pilote Python (pymongo).

Ce que je fais actuellement est (pseudo-code) :

for each document in update:
      existing_document = collection.find_one(document)
      if not existing_document:
           document['insertion_date'] = now
      else:
           document = existing_document
      document['last_update_date'] = now
      my_collection.save(document)

Mon problème est qu'il est très lent (40 minutes pour moins de 100 000 enregistrements, et j'en ai des millions dans la mise à jour). Je suis presque sûr qu'il y a quelque chose d'intégré pour faire cela, mais le document pour update() est mmmhhh.... un peu laconique.... ( http://www.mongodb.org/display/DOCS/Updating )

Quelqu'un peut-il me conseiller sur la manière de le faire plus rapidement ?

181voto

Van Nguyen Points 1664

On dirait que vous voulez faire un "upsert". MongoDB dispose d'un support intégré pour cela. Passez un paramètre supplémentaire à votre appel update() : {upsert:true}. Par exemple :

key = {'key':'value'}
data = {'key2':'value2', 'key3':'value3'};
coll.update(key, data, upsert=True); #In python upsert must be passed as a keyword argument

Cela remplace entièrement votre bloc if-find-else-update. Il va insérer si la clé n'existe pas et mettre à jour si elle existe.

Avant :

{"key":"value", "key2":"Ohai."}

Après :

{"key":"value", "key2":"value2", "key3":"value3"}

Vous pouvez également spécifier les données que vous souhaitez écrire :

data = {"$set":{"key2":"value2"}}

Maintenant, le document sélectionné ne mettra à jour que la valeur de "key2" et laissera tout le reste intact.

10 votes

C'est presque ce que je veux ! Comment puis-je ne pas toucher au champ insertion_date si l'objet est déjà présent ?

1 votes

LeMiz : Vous pouvez passer $set à la variable de données dans update pour choisir sélectivement ce qui doit être mis à jour.

34 votes

Pouvez-vous s'il vous plaît donner un exemple pour définir un champ lors de la première insertion et ne pas le mettre à jour s'il existe ? @VanNguyen

93voto

andy Points 885

À partir de MongoDB 2.4, vous pouvez utiliser $setOnInsert ( http://docs.mongodb.org/manual/reference/operator/setOnInsert/ )

Définissez 'insertion_date' en utilisant $setOnInsert et 'last_update_date' en utilisant $set dans votre commande upsert.

Pour transformer votre pseudo-code en un exemple fonctionnel :

now = datetime.utcnow()
for document in update:
    collection.update_one(
        filter={
            '_id': document['_id'],
        },
        update={
            '$setOnInsert': {
                'insertion_date': now,
            },
            '$set': {
                'last_update_date': now,
            },
        },
        upsert=True,
    )

6 votes

C'est correct, vous pouvez vérifier si un document correspond à un filtre, et insérer quelque chose s'il n'est pas trouvé, en utilisant $setOnInsert. Notez cependant qu'il y avait un bogue où vous ne pouviez pas $setOnInsert avec le champ _id - le système disait quelque chose comme "can't Mod the _id field". C'était un bogue, corrigé dans la v2.5.4 ou à peu près. Si vous voyez ce message ou ce problème, prenez simplement la dernière version.

0 votes

Cela devrait être la réponse acceptée.

22voto

Ram Rajamony Points 760

Vous pouvez toujours créer un index unique, ce qui amène MongoDB à rejeter une sauvegarde conflictuelle. Considérez ce qui suit réalisé à l'aide du shell mongodb :

> db.getCollection("test").insert ({a:1, b:2, c:3})
> db.getCollection("test").find()
{ "_id" : ObjectId("50c8e35adde18a44f284e7ac"), "a" : 1, "b" : 2, "c" : 3 }
> db.getCollection("test").ensureIndex ({"a" : 1}, {unique: true})
> db.getCollection("test").insert({a:2, b:12, c:13})      # This works
> db.getCollection("test").insert({a:1, b:12, c:13})      # This fails
E11000 duplicate key error index: foo.test.$a_1  dup key: { : 1.0 }

0 votes

Il est maintenant createIndex

0 votes

{"a" : 1}, {unique: true} signifie que le contenu du champ a doit être unique et aucun autre article ne peut avoir le même contenu ?

6voto

Yonsink Points 41

Je ne pense pas que mongodb supporte ce type d'upserting sélectif. J'ai le même problème que LeMiz, et en utilisant update(critères, newObj, upsert, multi) ne fonctionne pas correctement lorsqu'il s'agit d'un timestamp "created" et "updated". Soit l'instruction upsert suivante :

update( { "name": "abc" }, 
        { $set: { "created": "2010-07-14 11:11:11", 
                  "updated": "2010-07-14 11:11:11" }},
        true, true ) 

Scénario 1 : le document dont le nom est "abc" n'existe pas : Un nouveau document est créé avec 'name' = 'abc', 'created' = 2010-07-14 11:11:11, et 'updated' = 2010-07-14 11:11:11.

Scénario n° 2 : un document portant le "nom" de "abc" existe déjà avec les éléments suivants : nom' = 'abc', 'créé' = 2010-07-12 09:09:09, et 'mis à jour' = 2010-07-13 10:10:10. Après l'upsert, le document serait maintenant identique au résultat du scénario 1. Il n'y a aucun moyen de spécifier dans une upsert quels champs doivent être définis en cas d'insertion et quels champs doivent rester inchangés en cas de mise à jour.

Ma solution a consisté à créer un index unique sur le fichier Critère effectuer une insertion et, immédiatement après, effectuer une mise à jour uniquement sur le champ "mis à jour".

6voto

Meshach Jackson Points 81

1. Utilisez la mise à jour.

En s'inspirant de la réponse de Van Nguyen ci-dessus, utilisez update au lieu de save. Cela vous donne accès à l'option upsert.

NOTE : Cette méthode remplace l'ensemble du document lorsqu'il est trouvé ( Extrait de la documentation )

var conditions = { name: 'borne' }   , update = { $inc: { visits: 1 }} , options = { multi: true };

Model.update(conditions, update, options, callback);

function callback (err, numAffected) {   // numAffected is the number of updated documents })

1.a. Utilisez $set

Si vous souhaitez mettre à jour une sélection du document, mais pas l'ensemble, vous pouvez utiliser la méthode $set avec update. (encore une fois, Extrait de la documentation )... Donc, si vous voulez mettre...

var query = { name: 'borne' };  Model.update(query, ***{ name: 'jason borne' }***, options, callback)

Envoyez-le comme...

Model.update(query, ***{ $set: { name: 'jason borne' }}***, options, callback)

Cela permet d'éviter d'écraser accidentellement tout votre/vos document(s) avec { name: 'jason borne' } .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X