Added initial implementation of cascading document deletion.
The current implementation is still very basic and needs some polish. The essence of it is that each Document gets a new meta attribute called "delete_rules" that is a dictionary containing (documentclass, fieldname) as key and the actual delete rule as a value. (Possible values are DO_NOTHING, NULLIFY, CASCADE and DENY. Of those, only CASCADE is currently implented.)
This commit is contained in:
		| @@ -190,6 +190,8 @@ class DocumentMetaclass(type): | |||||||
|         new_class = super_new(cls, name, bases, attrs) |         new_class = super_new(cls, name, bases, attrs) | ||||||
|         for field in new_class._fields.values(): |         for field in new_class._fields.values(): | ||||||
|             field.owner_document = new_class |             field.owner_document = new_class | ||||||
|  |             if hasattr(field, 'delete_rule') and field.delete_rule: | ||||||
|  |                 field.document_type._meta['delete_rules'][(new_class, field.name)] = field.delete_rule | ||||||
|  |  | ||||||
|         module = attrs.get('__module__') |         module = attrs.get('__module__') | ||||||
|  |  | ||||||
| @@ -258,6 +260,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): | |||||||
|             'index_drop_dups': False, |             'index_drop_dups': False, | ||||||
|             'index_opts': {}, |             'index_opts': {}, | ||||||
|             'queryset_class': QuerySet, |             'queryset_class': QuerySet, | ||||||
|  |             'delete_rules': {}, | ||||||
|         } |         } | ||||||
|         meta.update(base_meta) |         meta.update(base_meta) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,9 +6,16 @@ from connection import _get_db | |||||||
| import pymongo | import pymongo | ||||||
|  |  | ||||||
|  |  | ||||||
| __all__ = ['Document', 'EmbeddedDocument', 'ValidationError', 'OperationError'] | __all__ = ['Document', 'EmbeddedDocument', 'ValidationError', 'OperationError', | ||||||
|  |         'DO_NOTHING', 'NULLIFY', 'CASCADE', 'DENY'] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Delete rules | ||||||
|  | DO_NOTHING = 0 | ||||||
|  | NULLIFY = 1 | ||||||
|  | CASCADE = 2 | ||||||
|  | DENY = 3 | ||||||
|  |  | ||||||
| class EmbeddedDocument(BaseDocument): | class EmbeddedDocument(BaseDocument): | ||||||
|     """A :class:`~mongoengine.Document` that isn't stored in its own |     """A :class:`~mongoengine.Document` that isn't stored in its own | ||||||
|     collection.  :class:`~mongoengine.EmbeddedDocument`\ s should be used as |     collection.  :class:`~mongoengine.EmbeddedDocument`\ s should be used as | ||||||
| @@ -92,6 +99,13 @@ class Document(BaseDocument): | |||||||
|  |  | ||||||
|         :param safe: check if the operation succeeded before returning |         :param safe: check if the operation succeeded before returning | ||||||
|         """ |         """ | ||||||
|  |         for rule_entry in self._meta['delete_rules']: | ||||||
|  |             document_cls, field_name = rule_entry | ||||||
|  |             rule = self._meta['delete_rules'][rule_entry] | ||||||
|  |  | ||||||
|  |             if rule == CASCADE: | ||||||
|  |                 document_cls.objects(**{field_name: self.id}).delete(safe=safe) | ||||||
|  |  | ||||||
|         id_field = self._meta['id_field'] |         id_field = self._meta['id_field'] | ||||||
|         object_id = self._fields[id_field].to_mongo(self[id_field]) |         object_id = self._fields[id_field].to_mongo(self[id_field]) | ||||||
|         try: |         try: | ||||||
| @@ -100,6 +114,17 @@ class Document(BaseDocument): | |||||||
|             message = u'Could not delete document (%s)' % err.message |             message = u'Could not delete document (%s)' % err.message | ||||||
|             raise OperationError(message) |             raise OperationError(message) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def register_delete_rule(cls, document_cls, field_name, rule): | ||||||
|  |         """This method registers the delete rules to apply when removing this | ||||||
|  |         object.  This could go into the Document class. | ||||||
|  |         """ | ||||||
|  |         if rule == DO_NOTHING: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         cls._meta['delete_rules'][(document_cls, field_name)] = rule | ||||||
|  |  | ||||||
|  |  | ||||||
|     def reload(self): |     def reload(self): | ||||||
|         """Reloads all attributes from the database. |         """Reloads all attributes from the database. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -417,12 +417,13 @@ class ReferenceField(BaseField): | |||||||
|     access (lazily). |     access (lazily). | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def __init__(self, document_type, **kwargs): |     def __init__(self, document_type, delete_rule=None, **kwargs): | ||||||
|         if not isinstance(document_type, basestring): |         if not isinstance(document_type, basestring): | ||||||
|             if not issubclass(document_type, (Document, basestring)): |             if not issubclass(document_type, (Document, basestring)): | ||||||
|                 raise ValidationError('Argument to ReferenceField constructor ' |                 raise ValidationError('Argument to ReferenceField constructor ' | ||||||
|                                       'must be a document class or a string') |                                       'must be a document class or a string') | ||||||
|         self.document_type_obj = document_type |         self.document_type_obj = document_type | ||||||
|  |         self.delete_rule = delete_rule | ||||||
|         super(ReferenceField, self).__init__(**kwargs) |         super(ReferenceField, self).__init__(**kwargs) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|   | |||||||
| @@ -624,6 +624,31 @@ class DocumentTest(unittest.TestCase): | |||||||
|  |  | ||||||
|         BlogPost.drop_collection() |         BlogPost.drop_collection() | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     def test_cascade_delete(self): | ||||||
|  |         """Ensure that a referenced document is also deleted upon deletion. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         class BlogPost(Document): | ||||||
|  |             meta = {'collection': 'blogpost_1'} | ||||||
|  |             content = StringField() | ||||||
|  |             author = ReferenceField(self.Person, delete_rule=CASCADE) | ||||||
|  |  | ||||||
|  |         self.Person.drop_collection() | ||||||
|  |         BlogPost.drop_collection() | ||||||
|  |  | ||||||
|  |         author = self.Person(name='Test User') | ||||||
|  |         author.save() | ||||||
|  |  | ||||||
|  |         post = BlogPost(content = 'Watched some TV') | ||||||
|  |         post.author = author | ||||||
|  |         post.save() | ||||||
|  |  | ||||||
|  |         # Delete the Person, which should lead to deletion of the BlogPost, too | ||||||
|  |         author.delete() | ||||||
|  |         self.assertEqual(len(BlogPost.objects), 0) | ||||||
|  |  | ||||||
|  |  | ||||||
|     def tearDown(self): |     def tearDown(self): | ||||||
|         self.Person.drop_collection() |         self.Person.drop_collection() | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user