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:
parent
4f3eacd72c
commit
86233bcdf5
@ -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()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user