class Collection(PyMongoCollection): """ A Mongo collection linked to an audit collection """ def __init__(self, database, main_collection_name, audit_collection_name, revision_field, **kwargs): # type: (Database, str, str, str, dict) -> None """ :param database: A PyMongo Database :type database: Database :param main_collection_name: Name of the main collection :type main_collection_name: str :param audit_collection_name: Name of the audit collection :type audit_collection_name: str :param revision_field: Name of the field to be added to any document in the main collection to reference the current revision in the audit collection :type revision_field: str :param kwargs: additional arguments :type kwargs: dict """ super(Collection, self).__init__(database, main_collection_name, **kwargs) self.main_collection = PyMongoCollection(database, main_collection_name, **kwargs) self.audit_collection = PyMongoCollection(database, audit_collection_name, **kwargs) self.audit_collection.create_index('document_id', unique=False, name='document_id') self.revision_field = revision_field def insert_one(self, document, bypass_document_validation=False, audit=None): # type: (dict, bool, dict) -> InsertOneResult """ The strategy of this method is to insert an audit_document into audit collection first and then to insert the actual document into the main collection. The audit_document carries a reference to the actual document _id. Inserting the actual document before the audition succeed jeopardises the audition integrity as a rollback can't be guaranteed in case of failure. A third update operation to put the actual document _id in the audit_document should be avoided to save the cost of the operation. For the audit_document to carry the reference to the actual document _id an optimistic lock strategy is used. The document _id is generated before any insertions. This saves one extra update operation on the audit collection. If the _id conflicts with one already existent in the database and the insertion of the actual document fails, then the audit_document inserted is removed to rollback the operation and to try again. :param document: The document to insert. Must be a mutable mapping type. If the document does not have an _id field one will be added automatically. :type document: dict :param bypass_document_validation: (optional) If ``True``, allows the write to opt-out of document level validation. Default is ``False``. :param audit: (optional) Additional audit data. The audit_document derived from the original document will be updated with entries in this dictionary. Default is ``None``. :type audit: dict """ while True: _id = ObjectId() document['_id'] = _id document[self.revision_field] = _id audit_document = dict(document) audit_document['document_id'] = _id if audit: audit_document.update(audit) inserted_audit_id = None try: inserted_audit_id = self.audit_collection.insert_one( audit_document, bypass_document_validation).inserted_id return self.main_collection.insert_one( document, bypass_document_validation) except DuplicateKeyError: try: if inserted_audit_id is not None: self.audit_collection.delete_one({'_id': _id}) except Exception: logging.exception( "An error occurred while cleaning failed insertion" " operation from the audit collection.\n" "An orphaned record may have been left in the audit" " collection.") raise