--- Revision None +++ Revision 303866353263 @@ -0,0 +1,75 @@ + def update_fields(self, record_id, fields, cached_record=None): + """Safely update a number of fields. 'fields' being a + dictionary with path_tuple: new_value for only the fields we + want to change the value of, where path_tuple is a tuple of + fieldnames indicating the path to the possibly nested field + we're interested in. old_record a the copy of the record we + most recently read from the database. + + In the case the underlying document was changed, we try to + merge, but only if none of the old values have changed. (i.e., + do not overwrite changes originating elsewhere.) + + This is slightly hairy, so that other code won't have to be. + """ + # Initially, the record in memory and in the db are the same + # as far as we know. (If they're not, we'll get a + # ResourceConflict later on, from which we can recover.) + if cached_record is None: + cached_record = self.db[record_id] + if isinstance(cached_record, Record): + cached_record = cached_record._data + record = copy.deepcopy(cached_record) + # Loop until either failure or success has been determined + while True: + modified = False + conflicts = {} + # loop through all the fields that need to be modified + for path, new_value in fields.items(): + if not isinstance(path, tuple): + path = (path,) + # Walk down in both copies of the record to the leaf + # node that represents the field, creating the path in + # the in memory record if necessary. + db_parent = record + cached_parent = cached_record + for field in path[:-1]: + db_parent = db_parent.setdefault(field, {}) + cached_parent = cached_parent.get(field, {}) + # Get the values of the fields in the two copies. + db_value = db_parent.get(path[-1]) + cached_value = cached_parent.get(path[-1]) + # If the value we intend to store is already in the + # database, we need do nothing, which is our favorite. + if db_value == new_value: + continue + # If the value in the db is different than the value + # our copy holds, we have a conflict. We could bail + # here, but we can give better feedback if we gather + # all the conflicts, so we continue the for loop + if db_value != cached_value: + conflicts[path] = (db_value, new_value) + continue + # Otherwise, it is safe to update the field with the + # new value. + modified = True + db_parent[path[-1]] = new_value + # If we had conflicts, we can bail. + if conflicts: + raise FieldsConflict(conflicts) + # If we made changes to the document, we'll need to save + # it. + if modified: + try: + self.db[record_id] = record + except ResourceConflict: + # We got a conflict, meaning the record has + # changed in the database since we last loaded it + # into memory. Let's get a fresh copy and try + # again. + record = self.db[record_id] + continue + # If we get here, nothing remains to be done, and we can + # take a well deserved break. + break +