summaryrefslogtreecommitdiff
path: root/django/db
diff options
context:
space:
mode:
Diffstat (limited to 'django/db')
-rw-r--r--django/db/models/base.py19
-rw-r--r--django/db/models/loading.py12
-rw-r--r--django/db/models/query.py116
3 files changed, 122 insertions, 25 deletions
diff --git a/django/db/models/base.py b/django/db/models/base.py
index a253f38f47..d7e2c299cc 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -10,7 +10,7 @@ from django.core import validators
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
from django.db.models.fields import AutoField, ImageField, FieldDoesNotExist
from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
-from django.db.models.query import delete_objects, Q
+from django.db.models.query import delete_objects, Q, CollectedObjects
from django.db.models.options import Options
from django.db import connection, transaction
from django.db.models import signals
@@ -365,17 +365,16 @@ class Model(object):
error_dict[f.name] = errors
return error_dict
- def _collect_sub_objects(self, seen_objs):
+ def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
"""
Recursively populates seen_objs with all objects related to this object.
- When done, seen_objs will be in the format:
- {model_class: {pk_val: obj, pk_val: obj, ...},
- model_class: {pk_val: obj, pk_val: obj, ...}, ...}
+ When done, seen_objs.items() will be in the format:
+ [(model_class, {pk_val: obj, pk_val: obj, ...}),
+ (model_class, {pk_val: obj, pk_val: obj, ...}),...]
"""
pk_val = self._get_pk_val()
- if pk_val in seen_objs.setdefault(self.__class__, {}):
+ if seen_objs.add(self.__class__, pk_val, self, parent, nullable):
return
- seen_objs.setdefault(self.__class__, {})[pk_val] = self
for related in self._meta.get_all_related_objects():
rel_opts_name = related.get_accessor_name()
@@ -385,16 +384,16 @@ class Model(object):
except ObjectDoesNotExist:
pass
else:
- sub_obj._collect_sub_objects(seen_objs)
+ sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
else:
for sub_obj in getattr(self, rel_opts_name).all():
- sub_obj._collect_sub_objects(seen_objs)
+ sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
def delete(self):
assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)
# Find all the objects than need to be deleted
- seen_objs = SortedDict()
+ seen_objs = CollectedObjects()
self._collect_sub_objects(seen_objs)
# Actually delete the objects
diff --git a/django/db/models/loading.py b/django/db/models/loading.py
index e62188adf7..6837e070ac 100644
--- a/django/db/models/loading.py
+++ b/django/db/models/loading.py
@@ -2,6 +2,8 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
+from django.utils.datastructures import SortedDict
+
import sys
import os
import threading
@@ -18,10 +20,10 @@ class AppCache(object):
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531.
__shared_state = dict(
# Keys of app_store are the model modules for each application.
- app_store = {},
+ app_store = SortedDict(),
# Mapping of app_labels to a dictionary of model names to model code.
- app_models = {},
+ app_models = SortedDict(),
# Mapping of app_labels to errors raised when trying to import the app.
app_errors = {},
@@ -133,7 +135,7 @@ class AppCache(object):
"""
self._populate()
if app_mod:
- return self.app_models.get(app_mod.__name__.split('.')[-2], {}).values()
+ return self.app_models.get(app_mod.__name__.split('.')[-2], SortedDict()).values()
else:
model_list = []
for app_entry in self.app_models.itervalues():
@@ -149,7 +151,7 @@ class AppCache(object):
"""
if seed_cache:
self._populate()
- return self.app_models.get(app_label, {}).get(model_name.lower())
+ return self.app_models.get(app_label, SortedDict()).get(model_name.lower())
def register_models(self, app_label, *models):
"""
@@ -159,7 +161,7 @@ class AppCache(object):
# Store as 'name: model' pair in a dictionary
# in the app_models dictionary
model_name = model._meta.object_name.lower()
- model_dict = self.app_models.setdefault(app_label, {})
+ model_dict = self.app_models.setdefault(app_label, SortedDict())
if model_name in model_dict:
# The same model may be imported via different paths (e.g.
# appname.models and project.appname.models). We use the source
diff --git a/django/db/models/query.py b/django/db/models/query.py
index fb6d116a6e..8714cffb7f 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -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)