Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I conditionally upsert a document in mongo?

I've got a document

{ key : 'key1', value : 'value1', update_time : 100 }

That I'd like to change only with more recent (greater) update times. What I'm doing now is:

def update_key1(new_value, new_time):
    record = find_one( { key : 'key1' } )
    if not record or record['update_time'] < new_time:
        update( { key : 'key1', value : new_value, update_time : new_time }, upsert=True)

Obviously this is an extra roundtrip to the db, but more importantly there's no lock on the document, and concurrent calls could result in a lower new_time's value remaining in the db. Is there a way to perform an upsert only if a condition is true?

EDIT: Just to clarify, the intention is not to create multiple documents for each key and then sort on lookup. Though that would solve my problem, these values change a lot and would waste a lot of space.

like image 266
Parker Avatar asked Dec 07 '12 15:12

Parker


2 Answers

Short of being able to do the whole thing atomically, there are two kinds of existing conditions where you want to make a change, and you can deal with each of them atomically:

  • no record for the key exists
  • a record for the key exists and its update_time is older than new_time

Update an existing record for key:

def update_if_stale(key, new_value, new_time):
    collection.update({'key': key,
                       'update_time': {'$lt': new_time}
                       },
                      {'$set': {'value': new_value,
                                'update_time': new_time
                                }
                       }
                      )

Insert if a record for key didn't exist before:

def insert_if_missing(key, new_value, new_time):
    collection.update({'key': key},
                      {'$setOnInsert': {'value': new_value,
                                        'update_time': new_time
                                        }
                       },
                      upsert=True
                      )

($setOnInsert was added in MongoDB 2.4)

You might be able to put those together to get what you need, e.g.:

def update_key(key, new_value, new_time):
    insert_if_missing(key, new_value, new_time)        
    update_if_stale(key, new_value, new_time)

However, depending on what remove/insert time scales might be possible in your system, you might need multiple calls (update/insert/update) or other shenanigans.

Aside: If you want a record missing the update_time field to be treated as a stale record to update, change {'$lt': new_time}} to {'$not': {'$gte': new_time}}

like image 156
rakslice Avatar answered Oct 11 '22 20:10

rakslice


If you add a unique constraint on key, then updateOne with an update_time filter and upsert=True will

  1. insert missing records
  2. update stale records
  3. throw an error when you try to update using stale input (because the update will not match the filter condition, and the insert will fail due to the constraint)
collection.updateOne({'key': key,
                      'update_time': {'$lt': new_time}
                      },
                     {'$set': {'value': new_value,
                               'update_time': new_time
                               }
                      },
                     upsert=True
                     )

You can catch errors and check for code: 11000 (unique constraint violations) to specifically handle those cases of having tried to update to a past state.

like image 30
ejohnson Avatar answered Oct 11 '22 19:10

ejohnson