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