def conditional_update(self, values, expected_values=None, filters=(), save_all=False, session=None, reflect_changes=True, order=None): """Compare-and-swap update. A conditional object update that, unlike normal update, will SAVE the contents of the update to the DB. Update will only occur in the DB and the object if conditions are met. If no expected_values are passed in we will default to make sure that all fields have not been changed in the DB. Since we cannot know the original value in the DB for dirty fields in the object those will be excluded. We have 4 different condition types we can use in expected_values: - Equality: {'status': 'available'} - Inequality: {'status': vol_obj.Not('deleting')} - In range: {'status': ['available', 'error'] - Not in range: {'status': vol_obj.Not(['in-use', 'attaching']) Method accepts additional filters, which are basically anything that can be passed to a sqlalchemy query's filter method, for example: .. code-block:: python [~sql.exists().where(models.Volume.id == models.Snapshot.volume_id)] We can select values based on conditions using Case objects in the 'values' argument. For example: .. code-block:: python has_snapshot_filter = sql.exists().where( models.Snapshot.volume_id == models.Volume.id) case_values = volume.Case([(has_snapshot_filter, 'has-snapshot')], else_='no-snapshot') volume.conditional_update({'status': case_values}, {'status': 'available'})) And we can use DB fields using model class attribute for example to store previous status in the corresponding field even though we don't know which value is in the db from those we allowed: .. code-block:: python volume.conditional_update({'status': 'deleting', 'previous_status': volume.model.status}, {'status': ('available', 'error')}) :param values: Dictionary of key-values to update in the DB. :param expected_values: Dictionary of conditions that must be met for the update to be executed. :param filters: Iterable with additional filters :param save_all: Object may have changes that are not in the DB, this will say whether we want those changes saved as well. :param session: Session to use for the update :param reflect_changes: If we want changes made in the database to be reflected in the versioned object. This may mean in some cases that we have to reload the object from the database. :param order: Specific order of fields in which to update the values :returns: number of db rows that were updated, which can be used as a boolean, since it will be 0 if we couldn't update the DB and 1 if we could, because we are using unique index id. """ if 'id' not in self.fields: msg = ( _('VersionedObject %s does not support conditional update.') % (self.obj_name())) raise NotImplementedError(msg) # If no conditions are set we will require object in DB to be unchanged if expected_values is None: changes = self.obj_what_changed() expected = { key: getattr(self, key) for key in self.fields.keys() if self.obj_attr_is_set(key) and key not in changes and key not in self.OPTIONAL_FIELDS } else: # Set the id in expected_values to limit conditional update to only # change this object expected = expected_values.copy() expected['id'] = self.id # If we want to save any additional changes the object has besides the # ones referred in values if save_all: changes = self.cinder_obj_get_changes() changes.update(values) values = changes result = db.conditional_update(self._context, self.model, values, expected, filters, order=order) # If we were able to update the DB then we need to update this object # as well to reflect new DB contents and clear the object's dirty flags # for those fields. if result and reflect_changes: # If we have used a Case, a db field or an expression in values we # don't know which value was used, so we need to read the object # back from the DB if any( isinstance(v, self.Case) or db.is_orm_value(v) for v in values.values()): # Read back object from DB obj = type(self).get_by_id(self._context, self.id) db_values = obj.obj_to_primitive()['versioned_object.data'] # Only update fields were changes were requested values = { field: db_values[field] for field, value in values.items() } # NOTE(geguileo): We don't use update method because our objects # will eventually move away from VersionedObjectDictCompat for key, value in values.items(): setattr(self, key, value) self.obj_reset_changes(values.keys()) return result
def conditional_update(self, values, expected_values=None, filters=(), save_all=False, session=None, reflect_changes=True, order=None): """Compare-and-swap update. A conditional object update that, unlike normal update, will SAVE the contents of the update to the DB. Update will only occur in the DB and the object if conditions are met. If no expected_values are passed in we will default to make sure that all fields have not been changed in the DB. Since we cannot know the original value in the DB for dirty fields in the object those will be excluded. We have 4 different condition types we can use in expected_values: - Equality: {'status': 'available'} - Inequality: {'status': vol_obj.Not('deleting')} - In range: {'status': ['available', 'error'] - Not in range: {'status': vol_obj.Not(['in-use', 'attaching']) Method accepts additional filters, which are basically anything that can be passed to a sqlalchemy query's filter method, for example: .. code-block:: python [~sql.exists().where(models.Volume.id == models.Snapshot.volume_id)] We can select values based on conditions using Case objects in the 'values' argument. For example: .. code-block:: python has_snapshot_filter = sql.exists().where( models.Snapshot.volume_id == models.Volume.id) case_values = volume.Case([(has_snapshot_filter, 'has-snapshot')], else_='no-snapshot') volume.conditional_update({'status': case_values}, {'status': 'available'})) And we can use DB fields using model class attribute for example to store previous status in the corresponding field even though we don't know which value is in the db from those we allowed: .. code-block:: python volume.conditional_update({'status': 'deleting', 'previous_status': volume.model.status}, {'status': ('available', 'error')}) :param values: Dictionary of key-values to update in the DB. :param expected_values: Dictionary of conditions that must be met for the update to be executed. :param filters: Iterable with additional filters :param save_all: Object may have changes that are not in the DB, this will say whether we want those changes saved as well. :param session: Session to use for the update :param reflect_changes: If we want changes made in the database to be reflected in the versioned object. This may mean in some cases that we have to reload the object from the database. :param order: Specific order of fields in which to update the values :returns: number of db rows that were updated, which can be used as a boolean, since it will be 0 if we couldn't update the DB and 1 if we could, because we are using unique index id. """ if 'id' not in self.fields: msg = (_('VersionedObject %s does not support conditional update.') % (self.obj_name())) raise NotImplementedError(msg) # If no conditions are set we will require object in DB to be unchanged if expected_values is None: changes = self.obj_what_changed() expected = {key: getattr(self, key) for key in self.fields.keys() if self.obj_attr_is_set(key) and key not in changes and key not in self.OPTIONAL_FIELDS} else: # Set the id in expected_values to limit conditional update to only # change this object expected = expected_values.copy() expected['id'] = self.id # If we want to save any additional changes the object has besides the # ones referred in values if save_all: changes = self.cinder_obj_get_changes() changes.update(values) values = changes result = db.conditional_update(self._context, self.model, values, expected, filters, order=order) # If we were able to update the DB then we need to update this object # as well to reflect new DB contents and clear the object's dirty flags # for those fields. if result and reflect_changes: # If we have used a Case, a db field or an expression in values we # don't know which value was used, so we need to read the object # back from the DB if any(isinstance(v, self.Case) or db.is_orm_value(v) for v in values.values()): # Read back object from DB obj = type(self).get_by_id(self._context, self.id) db_values = obj.obj_to_primitive()['versioned_object.data'] # Only update fields were changes were requested values = {field: db_values[field] for field, value in values.items()} # NOTE(geguileo): We don't use update method because our objects # will eventually move away from VersionedObjectDictCompat for key, value in values.items(): setattr(self, key, value) self.obj_reset_changes(values.keys()) return result