mirror of
https://github.com/django/django.git
synced 2025-09-26 12:09:19 +00:00
Fixed bug with Model.delete() which did not always delete objects in the right order.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@7722 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
7c621535a2
commit
b1851cca3e
3 changed files with 205 additions and 33 deletions
|
@ -16,6 +16,92 @@ ITER_CHUNK_SIZE = CHUNK_SIZE
|
|||
# Pull into this namespace for backwards compatibility
|
||||
EmptyResultSet = sql.EmptyResultSet
|
||||
|
||||
class CyclicDependency(Exception):
|
||||
pass
|
||||
|
||||
class CollectedObjects(object):
|
||||
"""
|
||||
A container that stores keys and lists of values along with
|
||||
remembering the parent objects for all the keys.
|
||||
|
||||
This is used for the database object deletion routines so that we
|
||||
can calculate the 'leaf' objects which should be deleted first.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.data = {}
|
||||
self.children = {}
|
||||
|
||||
def add(self, model, pk, obj, parent_model, nullable=False):
|
||||
"""
|
||||
Adds an item.
|
||||
model is the class of the object being added,
|
||||
pk is the primary key, obj is the object itself,
|
||||
parent_model is the model of the parent object
|
||||
that this object was reached through, nullable should
|
||||
be True if this relation is nullable.
|
||||
|
||||
If the item already existed in the structure,
|
||||
returns true, otherwise false.
|
||||
"""
|
||||
d = self.data.setdefault(model, SortedDict())
|
||||
retval = pk in d
|
||||
d[pk] = obj
|
||||
# Nullable relationships can be ignored -- they
|
||||
# are nulled out before deleting, and therefore
|
||||
# do not affect the order in which objects have
|
||||
# to be deleted.
|
||||
if parent_model is not None and not nullable:
|
||||
self.children.setdefault(parent_model, []).append(model)
|
||||
|
||||
return retval
|
||||
|
||||
def __contains__(self, key):
|
||||
return self.data.__contains__(key)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.data[key]
|
||||
|
||||
def __nonzero__(self):
|
||||
return bool(self.data)
|
||||
|
||||
def iteritems(self):
|
||||
for k in self.ordered_keys():
|
||||
yield k, self[k]
|
||||
|
||||
def items(self):
|
||||
return list(self.iteritems())
|
||||
|
||||
def keys(self):
|
||||
return self.ordered_keys()
|
||||
|
||||
def ordered_keys(self):
|
||||
"""
|
||||
Returns the models in the order that they should be
|
||||
dealth with i.e. models with no dependencies first.
|
||||
"""
|
||||
dealt_with = SortedDict()
|
||||
# Start with items that have no children
|
||||
models = self.data.keys()
|
||||
while len(dealt_with) < len(models):
|
||||
found = False
|
||||
for model in models:
|
||||
children = self.children.setdefault(model, [])
|
||||
if len([c for c in children if c not in dealt_with]) == 0:
|
||||
dealt_with[model] = None
|
||||
found = True
|
||||
if not found:
|
||||
raise CyclicDependency("There is a cyclic dependency of items to be processed.")
|
||||
|
||||
return dealt_with.keys()
|
||||
|
||||
def unordered_keys(self):
|
||||
"""
|
||||
Fallback for the case where is a cyclic dependency but we
|
||||
don't care.
|
||||
"""
|
||||
return self.data.keys()
|
||||
|
||||
class QuerySet(object):
|
||||
"Represents a lazy database lookup for a set of objects"
|
||||
def __init__(self, model=None, query=None):
|
||||
|
@ -275,7 +361,7 @@ class QuerySet(object):
|
|||
while 1:
|
||||
# Collect all the objects to be deleted in this chunk, and all the
|
||||
# objects that are related to the objects that are to be deleted.
|
||||
seen_objs = SortedDict()
|
||||
seen_objs = CollectedObjects()
|
||||
for object in del_query[:CHUNK_SIZE]:
|
||||
object._collect_sub_objects(seen_objs)
|
||||
|
||||
|
@ -682,19 +768,27 @@ def delete_objects(seen_objs):
|
|||
Iterate through a list of seen classes, and remove any instances that are
|
||||
referred to.
|
||||
"""
|
||||
ordered_classes = seen_objs.keys()
|
||||
ordered_classes.reverse()
|
||||
try:
|
||||
ordered_classes = seen_objs.keys()
|
||||
except CyclicDependency:
|
||||
# if there is a cyclic dependency, we cannot in general delete
|
||||
# the objects. However, if an appropriate transaction is set
|
||||
# up, or if the database is lax enough, it will succeed.
|
||||
# So for now, we go ahead and try anway.
|
||||
ordered_classes = seen_objs.unordered_keys()
|
||||
|
||||
obj_pairs = {}
|
||||
for cls in ordered_classes:
|
||||
seen_objs[cls] = seen_objs[cls].items()
|
||||
seen_objs[cls].sort()
|
||||
items = seen_objs[cls].items()
|
||||
items.sort()
|
||||
obj_pairs[cls] = items
|
||||
|
||||
# Pre notify all instances to be deleted
|
||||
for pk_val, instance in seen_objs[cls]:
|
||||
for pk_val, instance in items:
|
||||
dispatcher.send(signal=signals.pre_delete, sender=cls,
|
||||
instance=instance)
|
||||
|
||||
pk_list = [pk for pk,instance in seen_objs[cls]]
|
||||
pk_list = [pk for pk,instance in items]
|
||||
del_query = sql.DeleteQuery(cls, connection)
|
||||
del_query.delete_batch_related(pk_list)
|
||||
|
||||
|
@ -705,15 +799,17 @@ def delete_objects(seen_objs):
|
|||
|
||||
# Now delete the actual data
|
||||
for cls in ordered_classes:
|
||||
seen_objs[cls].reverse()
|
||||
pk_list = [pk for pk,instance in seen_objs[cls]]
|
||||
items = obj_pairs[cls]
|
||||
items.reverse()
|
||||
|
||||
pk_list = [pk for pk,instance in items]
|
||||
del_query = sql.DeleteQuery(cls, connection)
|
||||
del_query.delete_batch(pk_list)
|
||||
|
||||
# Last cleanup; set NULLs where there once was a reference to the
|
||||
# object, NULL the primary key of the found objects, and perform
|
||||
# post-notification.
|
||||
for pk_val, instance in seen_objs[cls]:
|
||||
for pk_val, instance in items:
|
||||
for field in cls._meta.fields:
|
||||
if field.rel and field.null and field.rel.to in seen_objs:
|
||||
setattr(instance, field.attname, None)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue