Django

Code

Changeset 7722

Show
Ignore:
Timestamp:
06/21/08 15:57:05 (3 months ago)
Author:
lukeplant
Message:

Fixed bug with Model.delete() which did not always delete objects in the right order.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/db/models/base.py

    r7720 r7722  
    1111from django.db.models.fields import AutoField, ImageField, FieldDoesNotExist 
    1212from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField 
    13 from django.db.models.query import delete_objects, Q 
     13from django.db.models.query import delete_objects, Q, CollectedObjects 
    1414from django.db.models.options import Options, AdminOptions 
    1515from django.db import connection, transaction 
     
    369369        return error_dict 
    370370 
    371     def _collect_sub_objects(self, seen_objs): 
     371    def _collect_sub_objects(self, seen_objs, parent=None, nullable=False): 
    372372        """ 
    373373        Recursively populates seen_objs with all objects related to this object. 
    374         When done, seen_objs will be in the format: 
    375             {model_class: {pk_val: obj, pk_val: obj, ...}
    376              model_class: {pk_val: obj, pk_val: obj, ...}, ...} 
     374        When done, seen_objs.items() will be in the format: 
     375            [(model_class, {pk_val: obj, pk_val: obj, ...})
     376             (model_class, {pk_val: obj, pk_val: obj, ...}),...] 
    377377        """ 
    378378        pk_val = self._get_pk_val() 
    379         if pk_val in seen_objs.setdefault(self.__class__, {}): 
     379        if seen_objs.add(self.__class__, pk_val, self, parent, nullable): 
    380380            return 
    381         seen_objs[self.__class__][pk_val] = self 
    382381 
    383382        for related in self._meta.get_all_related_objects(): 
     
    389388                    pass 
    390389                else: 
    391                     sub_obj._collect_sub_objects(seen_objs
     390                    sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null
    392391            else: 
    393392                for sub_obj in getattr(self, rel_opts_name).all(): 
    394                     sub_obj._collect_sub_objects(seen_objs
     393                    sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null
    395394 
    396395    def delete(self): 
     
    398397 
    399398        # Find all the objects than need to be deleted 
    400         seen_objs = SortedDict() 
     399        seen_objs = CollectedObjects() 
    401400        self._collect_sub_objects(seen_objs) 
    402401 
  • django/trunk/django/db/models/query.py

    r7636 r7722  
    1616# Pull into this namespace for backwards compatibility 
    1717EmptyResultSet = sql.EmptyResultSet 
     18 
     19class CyclicDependency(Exception): 
     20    pass 
     21 
     22class CollectedObjects(object): 
     23    """ 
     24    A container that stores keys and lists of values along with 
     25    remembering the parent objects for all the keys. 
     26 
     27    This is used for the database object deletion routines so that we 
     28    can calculate the 'leaf' objects which should be deleted first. 
     29    """ 
     30 
     31    def __init__(self): 
     32        self.data = {} 
     33        self.children = {} 
     34 
     35    def add(self, model, pk, obj, parent_model, nullable=False): 
     36        """ 
     37        Adds an item. 
     38        model is the class of the object being added, 
     39        pk is the primary key, obj is the object itself,  
     40        parent_model is the model of the parent object 
     41        that this object was reached through, nullable should 
     42        be True if this relation is nullable. 
     43 
     44        If the item already existed in the structure, 
     45        returns true, otherwise false. 
     46        """ 
     47        d = self.data.setdefault(model, SortedDict()) 
     48        retval = pk in d 
     49        d[pk] = obj 
     50        # Nullable relationships can be ignored -- they 
     51        # are nulled out before deleting, and therefore 
     52        # do not affect the order in which objects have 
     53        # to be deleted. 
     54        if parent_model is not None and not nullable: 
     55            self.children.setdefault(parent_model, []).append(model) 
     56 
     57        return retval 
     58 
     59    def __contains__(self, key): 
     60        return self.data.__contains__(key) 
     61 
     62    def __getitem__(self, key): 
     63        return self.data[key] 
     64 
     65    def __nonzero__(self): 
     66        return bool(self.data) 
     67 
     68    def iteritems(self): 
     69        for k in self.ordered_keys(): 
     70            yield k, self[k] 
     71 
     72    def items(self): 
     73        return list(self.iteritems()) 
     74 
     75    def keys(self): 
     76        return self.ordered_keys() 
     77 
     78    def ordered_keys(self): 
     79        """ 
     80        Returns the models in the order that they should be  
     81        dealth with i.e. models with no dependencies first. 
     82        """ 
     83        dealt_with = SortedDict() 
     84        # Start with items that have no children 
     85        models = self.data.keys() 
     86        while len(dealt_with) < len(models): 
     87            found = False 
     88            for model in models: 
     89                children = self.children.setdefault(model, []) 
     90                if len([c for c in children if c not in dealt_with]) == 0: 
     91                    dealt_with[model] = None 
     92                    found = True 
     93            if not found: 
     94                raise CyclicDependency("There is a cyclic dependency of items to be processed.") 
     95             
     96        return dealt_with.keys() 
     97 
     98    def unordered_keys(self): 
     99        """ 
     100        Fallback for the case where is a cyclic dependency but we 
     101        don't care. 
     102        """ 
     103        return self.data.keys() 
    18104 
    19105class QuerySet(object): 
     
    276362            # Collect all the objects to be deleted in this chunk, and all the 
    277363            # objects that are related to the objects that are to be deleted. 
    278             seen_objs = SortedDict() 
     364            seen_objs = CollectedObjects() 
    279365            for object in del_query[:CHUNK_SIZE]: 
    280366                object._collect_sub_objects(seen_objs) 
     
    683769    referred to. 
    684770    """ 
    685     ordered_classes = seen_objs.keys() 
    686     ordered_classes.reverse() 
    687  
     771    try: 
     772        ordered_classes = seen_objs.keys() 
     773    except CyclicDependency: 
     774        # if there is a cyclic dependency, we cannot in general delete 
     775        # the objects.  However, if an appropriate transaction is set 
     776        # up, or if the database is lax enough, it will succeed.  
     777        # So for now, we go ahead and try anway. 
     778        ordered_classes = seen_objs.unordered_keys() 
     779 
     780    obj_pairs = {} 
    688781    for cls in ordered_classes: 
    689         seen_objs[cls] = seen_objs[cls].items() 
    690         seen_objs[cls].sort() 
     782        items = seen_objs[cls].items() 
     783        items.sort() 
     784        obj_pairs[cls] = items 
    691785 
    692786        # Pre notify all instances to be deleted 
    693         for pk_val, instance in seen_objs[cls]
     787        for pk_val, instance in items
    694788            dispatcher.send(signal=signals.pre_delete, sender=cls, 
    695789                    instance=instance) 
    696790 
    697         pk_list = [pk for pk,instance in seen_objs[cls]
     791        pk_list = [pk for pk,instance in items
    698792        del_query = sql.DeleteQuery(cls, connection) 
    699793        del_query.delete_batch_related(pk_list) 
     
    706800    # Now delete the actual data 
    707801    for cls in ordered_classes: 
    708         seen_objs[cls].reverse() 
    709         pk_list = [pk for pk,instance in seen_objs[cls]] 
     802        items = obj_pairs[cls] 
     803        items.reverse() 
     804 
     805        pk_list = [pk for pk,instance in items] 
    710806        del_query = sql.DeleteQuery(cls, connection) 
    711807        del_query.delete_batch(pk_list) 
     
    714810        # object, NULL the primary key of the found objects, and perform 
    715811        # post-notification. 
    716         for pk_val, instance in seen_objs[cls]
     812        for pk_val, instance in items
    717813            for field in cls._meta.fields: 
    718814                if field.rel and field.null and field.rel.to in seen_objs: 
  • django/trunk/tests/modeltests/delete/models.py

    r7721 r7722  
    3434# case things will be nice), or find Ds first. 
    3535 
     36# Some mutually dependent models, but nullable 
     37class E(DefaultRepr, models.Model): 
     38    f = models.ForeignKey('F', null=True, related_name='e_rel') 
     39 
     40class F(DefaultRepr, models.Model): 
     41    e = models.ForeignKey(E, related_name='f_rel') 
     42 
    3643 
    3744__test__ = {'API_TESTS': """ 
     45# First, some tests for the datastructure we use 
     46 
     47>>> from django.db.models.query import CollectedObjects 
     48 
     49>>> g = CollectedObjects() 
     50>>> g.add("key1", 1, "item1", None) 
     51False 
     52>>> g["key1"] 
     53{1: 'item1'} 
     54>>> g.add("key2", 1, "item1", "key1") 
     55False 
     56>>> g.add("key2", 2, "item2", "key1") 
     57False 
     58>>> g["key2"] 
     59{1: 'item1', 2: 'item2'} 
     60>>> g.add("key3", 1, "item1", "key1") 
     61False 
     62>>> g.add("key3", 1, "item1", "key2") 
     63True 
     64>>> g.ordered_keys() 
     65['key3', 'key2', 'key1'] 
     66 
     67>>> g.add("key2", 1, "item1", "key3") 
     68True 
     69>>> g.ordered_keys() 
     70Traceback (most recent call last): 
     71    ... 
     72CyclicDependency: There is a cyclic dependency of items to be processed. 
     73 
     74 
     75 
    3876# Due to the way that transactions work in the test harness, 
    3977# doing m.delete() here can work but fail in a real situation, 
     
    4381# Also, it is possible that the order is correct 'accidentally', due 
    4482# solely to order of imports etc.  To check this, we set the order 
    45 # that 'get_models()' will retrieve to a known 'tricky' order, and 
    46 # then try again with the reverse and try again.  Slightly naughty 
    47 # access to internals here. 
     83# that 'get_models()' will retrieve to a known 'nice' order, and 
     84# then try again with a known 'tricky' order.  Slightly naughty 
     85# access to internals here :-) 
    4886 
    49 >>> from django.utils.datastructures import SortedDict 
    5087>>> from django.db.models.loading import cache 
    5188 
     
    5693>>> del C._meta._related_objects_cache 
    5794>>> del D._meta._related_objects_cache 
    58  
    59  
    6095 
    6196>>> a1 = A() 
     
    68103>>> d1.save() 
    69104 
    70 >>> sd = SortedDict() 
    71 >>> a1._collect_sub_objects(sd
    72 >>> list(reversed(sd.keys())
     105>>> o = CollectedObjects() 
     106>>> a1._collect_sub_objects(o
     107>>> o.keys(
    73108[<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>] 
    74109>>> a1.delete() 
     
    81116>>> del D._meta._related_objects_cache 
    82117 
    83  
    84118>>> a2 = A() 
    85119>>> a2.save() 
     
    91125>>> d2.save() 
    92126 
    93 >>> sd2 = SortedDict() 
    94 >>> a2._collect_sub_objects(sd2
    95 >>> list(reversed(sd2.keys())
     127>>> o = CollectedObjects() 
     128>>> a2._collect_sub_objects(o
     129>>> o.keys(
    96130[<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>] 
    97131>>> a2.delete() 
    98132 
     133# Tests for nullable related fields 
    99134 
     135>>> g = CollectedObjects() 
     136>>> g.add("key1", 1, "item1", None) 
     137False 
     138>>> g.add("key2", 1, "item1", "key1", nullable=True) 
     139False 
     140>>> g.add("key1", 1, "item1", "key2") 
     141True 
     142>>> g.ordered_keys() 
     143['key1', 'key2'] 
     144 
     145>>> e1 = E() 
     146>>> e1.save() 
     147>>> f1 = F(e=e1) 
     148>>> f1.save() 
     149>>> e1.f = f1 
     150>>> e1.save() 
     151 
     152# Since E.f is nullable, we should delete F first (after nulling out 
     153# the E.f field), then E. 
     154 
     155>>> o = CollectedObjects() 
     156>>> e1._collect_sub_objects(o) 
     157>>> o.keys() 
     158[<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>] 
     159 
     160>>> e1.delete() 
     161 
     162>>> e2 = E() 
     163>>> e2.save() 
     164>>> f2 = F(e=e2) 
     165>>> f2.save() 
     166>>> e2.f = f2 
     167>>> e2.save() 
     168 
     169# Same deal as before, though we are starting from the other object. 
     170 
     171>>> o = CollectedObjects() 
     172>>> f2._collect_sub_objects(o) 
     173>>> o.keys() 
     174[<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>] 
     175 
     176>>> f2.delete() 
    100177 
    101178"""