Django

Code

Ticket #7539: 7539.on_delete.diff

File 7539.on_delete.diff, 18.2 kB (added by emulbreh, 5 months ago)
  • tests/modeltests/delete/models.py

    old new  
    4646 
    4747## First, test the CollectedObjects data structure directly 
    4848 
    49 >>> from django.db.models.query import CollectedObjects 
     49>>> from django.db.models.query_utils import CollectedFields, CollectedObjects 
    5050 
    5151>>> g = CollectedObjects() 
    5252>>> g.add("key1", 1, "item1", None) 
     
    112112>>> d1 = D(c=c1, a=a1) 
    113113>>> d1.save() 
    114114 
    115 >>> o = CollectedObjects() 
    116 >>> a1._collect_sub_objects(o
     115>>> o, f = CollectedObjects(), CollectedFields() 
     116>>> a1._collect_sub_objects(o, f
    117117>>> o.keys() 
    118118[<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>] 
     119>>> f.keys() 
     120[] 
    119121>>> a1.delete() 
    120122 
    121123# Same again with a known bad order 
     
    131133>>> d2 = D(c=c2, a=a2) 
    132134>>> d2.save() 
    133135 
    134 >>> o = CollectedObjects() 
    135 >>> a2._collect_sub_objects(o
     136>>> o, f = CollectedObjects(), CollectedFields() 
     137>>> a2._collect_sub_objects(o, f
    136138>>> o.keys() 
    137139[<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>] 
     140>>> f.keys() 
     141[] 
    138142>>> a2.delete() 
    139143 
    140144### Tests for models E,F - nullable related fields ### 
     
    163167# Since E.f is nullable, we should delete F first (after nulling out 
    164168# the E.f field), then E. 
    165169 
    166 >>> o = CollectedObjects() 
    167 >>> e1._collect_sub_objects(o
     170>>> o, f = CollectedObjects(), CollectedFields() 
     171>>> e1._collect_sub_objects(o, f
    168172>>> o.keys() 
    169173[<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>] 
     174>>> f.keys() 
     175[<class 'modeltests.delete.models.E'>] 
    170176 
    171 # temporarily replace the UpdateQuery class to verify that E.f is actually nulled out first 
    172 >>> import django.db.models.sql 
    173 >>> class LoggingUpdateQuery(django.db.models.sql.UpdateQuery): 
    174 ...     def clear_related(self, related_field, pk_list): 
    175 ...         print "CLEARING FIELD",related_field.name 
    176 ...         return super(LoggingUpdateQuery, self).clear_related(related_field, pk_list) 
    177 >>> original_class = django.db.models.sql.UpdateQuery 
    178 >>> django.db.models.sql.UpdateQuery = LoggingUpdateQuery 
    179177>>> e1.delete() 
    180 CLEARING FIELD f 
    181178 
    182179>>> e2 = E() 
    183180>>> e2.save() 
     
    188185 
    189186# Same deal as before, though we are starting from the other object. 
    190187 
    191 >>> o = CollectedObjects() 
    192 >>> f2._collect_sub_objects(o
     188>>> o, f = CollectedObjects(), CollectedFields() 
     189>>> f2._collect_sub_objects(o, f
    193190>>> o.keys() 
    194 [<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>] 
     191[<class 'modeltests.delete.models.F'>] 
     192>>> f.keys() 
     193[<class 'modeltests.delete.models.E'>] 
    195194 
    196195>>> f2.delete() 
    197 CLEARING FIELD f 
    198  
    199 # Put this back to normal 
    200 >>> django.db.models.sql.UpdateQuery = original_class 
    201196""" 
    202197} 
  • django/db/models/base.py

    old new  
    88from django.db.models.fields import AutoField, FieldDoesNotExist 
    99from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField 
    1010from django.db.models.query import delete_objects, Q 
    11 from django.db.models.query_utils import CollectedObjects, DeferredAttribute 
     11from django.db.models.query_utils import CollectedFields, CollectedObjects, DeferredAttribute 
    1212from django.db.models.options import Options 
    13 from django.db import connection, transaction, DatabaseError 
     13from django.db import connection, transaction, DatabaseError, IntegrityError 
    1414from django.db.models import signals 
     15from django.db.models.fields.related import CASCADE, PROTECT, SET_NULL, SET_DEFAULT, DO_NOTHING 
    1516from django.db.models.loading import register_models, get_model 
    1617from django.utils.functional import curry 
    1718from django.utils.encoding import smart_str, force_unicode, smart_unicode 
     
    513514 
    514515    save_base.alters_data = True 
    515516 
    516     def _collect_sub_objects(self, seen_objs, parent=None, nullable=False): 
     517    def _collect_sub_objects(self, seen_objs, fields_to_set, parent=None, nullable=False): 
    517518        """ 
    518519        Recursively populates seen_objs with all objects related to this 
    519520        object. 
     
    525526        pk_val = self._get_pk_val() 
    526527        if seen_objs.add(self.__class__, pk_val, self, parent, nullable): 
    527528            return 
     529             
     530        def _handle_sub_obj(related, sub_obj): 
     531            on_delete = related.field.rel.on_delete 
     532            if on_delete is None: 
     533                #If no explicit on_delete option is specified, use the old 
     534                #django behavior as the default: SET_NULL if the foreign 
     535                #key is nullable, otherwise CASCADE. 
     536                if related.field.null: 
     537                    on_delete = SET_NULL 
     538                else: 
     539                    on_delete = CASCADE 
     540            if on_delete == DO_NOTHING: 
     541                return 
     542            elif on_delete == CASCADE: 
     543                sub_obj._collect_sub_objects(seen_objs, fields_to_set, self.__class__) 
     544            elif on_delete == SET_NULL: 
     545                fields_to_set.add(sub_obj, related.field, None) 
     546            elif on_delete == SET_DEFAULT: 
     547                fields_to_set.add(sub_obj, related.field, related.field.get_default()) 
     548            elif on_delete == PROTECT: 
     549                msg = '[Django] Cannot delete a parent object: a foreign key constraint fails (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % ( 
     550                    sub_obj.__class__, 
     551                    sub_obj._get_pk_val(), 
     552                    self.__class__, 
     553                    pk_val, 
     554                    ) 
     555                raise IntegrityError(msg) 
    528556 
    529557        for related in self._meta.get_all_related_objects(): 
    530558            rel_opts_name = related.get_accessor_name() 
     
    534562                except ObjectDoesNotExist: 
    535563                    pass 
    536564                else: 
    537                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null
     565                    _handle_sub_obj(related, sub_obj
    538566            else: 
    539567                # To make sure we can access all elements, we can't use the 
    540568                # normal manager on the related object. So we work directly 
     
    547575                    raise AssertionError("Should never get here.") 
    548576                delete_qs = rel_descriptor.delete_manager(self).all() 
    549577                for sub_obj in delete_qs: 
    550                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null
     578                    _handle_sub_obj(related, sub_obj
    551579 
    552580        # Handle any ancestors (for the model-inheritance case). We do this by 
    553581        # traversing to the most remote parent classes -- those with no parents 
     
    562590                continue 
    563591            # At this point, parent_obj is base class (no ancestor models). So 
    564592            # delete it and all its descendents. 
    565             parent_obj._collect_sub_objects(seen_objs
     593            parent_obj._collect_sub_objects(seen_objs, fields_to_set
    566594 
    567595    def delete(self): 
    568596        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) 
    569597 
    570598        # Find all the objects than need to be deleted. 
    571599        seen_objs = CollectedObjects() 
    572         self._collect_sub_objects(seen_objs) 
     600        fields_to_set = CollectedFields() 
     601        self._collect_sub_objects(seen_objs, fields_to_set) 
    573602 
    574603        # Actually delete the objects. 
    575         delete_objects(seen_objs
     604        delete_objects(seen_objs, fields_to_set
    576605 
    577606    delete.alters_data = True 
    578607 
  • django/db/models/fields/related.py

    old new  
    2020 
    2121pending_lookups = {} 
    2222 
     23class CASCADE(object): 
     24    pass 
     25class PROTECT(object): 
     26    pass 
     27class SET_NULL(object): 
     28    pass 
     29class SET_DEFAULT(object): 
     30    pass 
     31class DO_NOTHING(object): 
     32    pass 
     33ALLOWED_ON_DELETE_ACTION_TYPES = set([None, CASCADE, PROTECT, SET_NULL, SET_DEFAULT, DO_NOTHING]) 
     34 
    2335def add_lazy_relation(cls, field, relation, operation): 
    2436    """ 
    2537    Adds a lookup on ``cls`` when a related field is defined using a string, 
     
    218230        # object you just set. 
    219231        setattr(instance, self.cache_name, value) 
    220232        setattr(value, self.related.field.get_cache_name(), instance) 
     233         
    221234 
    222235class ReverseSingleRelatedObjectDescriptor(object): 
    223236    # This class provides the functionality that makes the related-object 
     
    628641 
    629642class ManyToOneRel(object): 
    630643    def __init__(self, to, field_name, related_name=None, 
    631             limit_choices_to=None, lookup_overrides=None, parent_link=False): 
     644            limit_choices_to=None, lookup_overrides=None, parent_link=False, 
     645            on_delete=None): 
    632646        try: 
    633647            to._meta 
    634648        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT 
     
    641655        self.lookup_overrides = lookup_overrides or {} 
    642656        self.multiple = True 
    643657        self.parent_link = parent_link 
     658        self.on_delete = on_delete 
    644659 
    645660    def get_related_field(self): 
    646661        """ 
     
    655670 
    656671class OneToOneRel(ManyToOneRel): 
    657672    def __init__(self, to, field_name, related_name=None, 
    658             limit_choices_to=None, lookup_overrides=None, parent_link=False): 
     673            limit_choices_to=None, lookup_overrides=None, parent_link=False,  
     674            on_delete=None): 
    659675        super(OneToOneRel, self).__init__(to, field_name, 
    660676                related_name=related_name, limit_choices_to=limit_choices_to, 
    661                 lookup_overrides=lookup_overrides, parent_link=parent_link) 
     677                lookup_overrides=lookup_overrides, parent_link=parent_link, 
     678                on_delete=on_delete) 
    662679        self.multiple = False 
    663680 
    664681class ManyToManyRel(object): 
     
    697714            related_name=kwargs.pop('related_name', None), 
    698715            limit_choices_to=kwargs.pop('limit_choices_to', None), 
    699716            lookup_overrides=kwargs.pop('lookup_overrides', None), 
    700             parent_link=kwargs.pop('parent_link', False)) 
     717            parent_link=kwargs.pop('parent_link', False), 
     718            on_delete=kwargs.pop('on_delete', None)) 
    701719        Field.__init__(self, **kwargs) 
    702720 
    703721        self.db_index = True 
     
    742760            target = self.rel.to._meta.db_table 
    743761        cls._meta.duplicate_targets[self.column] = (target, "o2m") 
    744762 
     763        on_delete = self.rel.on_delete 
     764        if on_delete not in ALLOWED_ON_DELETE_ACTION_TYPES: 
     765            raise ValueError("Invalid value 'on_delete=%s' specified for %s %s.%s." % (on_delete, type(self).__name__, cls.__name__, name)) 
     766        if on_delete == SET_NULL and not self.null: 
     767            specification = "'on_delete=SET_NULL'" 
     768            raise ValueError("%s specified for %s '%s.%s', but the field is not nullable." % (specification, type(self).__name__, cls.__name__, name)) 
     769        if on_delete == SET_DEFAULT and not self.has_default(): 
     770            specification = "'on_delete=SET_DEFAULT'" 
     771            raise ValueError("%s specified for %s '%s.%s', but the field has no default value." % (specification, type(self).__name__, cls.__name__, name)) 
     772 
    745773    def contribute_to_related_class(self, cls, related): 
    746774        setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) 
    747775 
  • django/db/models/__init__.py

    old new  
    1111from django.db.models.fields.subclassing import SubfieldBase 
    1212from django.db.models.fields.files import FileField, ImageField 
    1313from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel 
     14from django.db.models.fields.related import CASCADE, PROTECT, SET_NULL, SET_DEFAULT 
    1415from django.db.models import signals 
    1516 
    1617# Admin stages. 
  • django/db/models/query.py

    old new  
    55from copy import deepcopy 
    66from django.db import connection, transaction, IntegrityError 
    77from django.db.models.aggregates import Aggregate 
     8from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE 
    89from django.db.models.fields import DateField 
    9 from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory 
     10from django.db.models.query_utils import Q, select_related_descend, CollectedFields, CollectedObjects, CyclicDependency, deferred_class_factory 
    1011from django.db.models import signals, sql 
    1112 
    1213# Used to control how many objects are worked with at once in some cases (e.g. 
     
    384385            # Collect all the objects to be deleted in this chunk, and all the 
    385386            # objects that are related to the objects that are to be deleted. 
    386387            seen_objs = CollectedObjects(seen_objs) 
     388            fields_to_set = CollectedFields() 
    387389            for object in del_query[:CHUNK_SIZE]: 
    388                 object._collect_sub_objects(seen_objs
     390                object._collect_sub_objects(seen_objs, fields_to_set
    389391 
    390392            if not seen_objs: 
    391393                break 
    392             delete_objects(seen_objs
     394            delete_objects(seen_objs, fields_to_set
    393395 
    394396        # Clear the result cache, in case this QuerySet gets reused. 
    395397        self._result_cache = None 
     
    10001002                setattr(obj, f.get_cache_name(), rel_obj) 
    10011003    return obj, index_end 
    10021004 
    1003 def delete_objects(seen_objs): 
     1005def delete_objects(seen_objs, fields_to_set): 
    10041006    """ 
    10051007    Iterate through a list of seen classes, and remove any instances that are 
    10061008    referred to. 
     
    10211023 
    10221024    obj_pairs = {} 
    10231025    try: 
     1026        fields_to_set.execute_updates() 
     1027 
    10241028        for cls in ordered_classes: 
    10251029            items = seen_objs[cls].items() 
    10261030            items.sort() 
     
    10341038            del_query = sql.DeleteQuery(cls, connection) 
    10351039            del_query.delete_batch_related(pk_list) 
    10361040 
    1037             update_query = sql.UpdateQuery(cls, connection) 
    1038             for field, model in cls._meta.get_fields_with_model(): 
    1039                 if (field.rel and field.null and field.rel.to in seen_objs and 
    1040                         filter(lambda f: f.column == field.rel.get_related_field().column, 
    1041                         field.rel.to._meta.fields)): 
    1042                     if model: 
    1043                         sql.UpdateQuery(model, connection).clear_related(field, 
    1044                                 pk_list) 
    1045                     else: 
    1046                         update_query.clear_related(field, pk_list) 
    1047  
    10481041        # Now delete the actual data. 
    10491042        for cls in ordered_classes: 
    10501043            items = obj_pairs[cls] 
     
    10541047            del_query = sql.DeleteQuery(cls, connection) 
    10551048            del_query.delete_batch(pk_list) 
    10561049 
    1057             # Last cleanup; set NULLs where there once was a reference to the 
    1058             # object, NULL the primary key of the found objects, and perform 
    1059             # post-notification. 
    1060             for pk_val, instance in items: 
    1061                 for field in cls._meta.fields: 
    1062                     if field.rel and field.null and field.rel.to in seen_objs: 
    1063                         setattr(instance, field.attname, None) 
     1050        fields_to_set.update_instances() 
    10641051 
     1052        for cls in ordered_classes: 
     1053            items = obj_pairs[cls] 
     1054            items.reverse() 
     1055            for pk_val, instance in items: 
    10651056                signals.post_delete.send(sender=cls, instance=instance) 
    10661057                setattr(instance, cls._meta.pk.attname, None) 
    10671058 
  • django/db/models/query_utils.py

    old new  
    124124        """ 
    125125        return self.data.keys() 
    126126 
     127class CollectedFields(object): 
     128    """ 
     129    A container that stores the model object and field 
     130    for fields that need to  be set to enforce on_delete=SET_NULL 
     131    and on_delete=SET_DEFAULT ForeigKey constraints. 
     132    """ 
     133 
     134    def __init__(self): 
     135        # {model: {(field, value): set([instances])}} 
     136        self.data = {} 
     137 
     138    def add(self, obj, field, value): 
     139        """ 
     140        Adds an item. 
     141        model is the class of the object being added, 
     142        field is the field to be set, 
     143        value is the value it needs to be set to. 
     144        """ 
     145        d = self.data.setdefault(obj.__class__, dict()) 
     146        instances = d.setdefault((field, value), set()) 
     147        instances.add(obj) 
     148     
     149    def execute_updates(self): 
     150        from django.db.models import connection, sql 
     151        for model, instances_for_fieldvalues in self.data.iteritems():            
     152            for (field, value), instances in instances_for_fieldvalues.iteritems():                 
     153                pk_field = model._meta.pk 
     154                pk_list = [obj.pk for obj in instances] 
     155                for offset in range(0, len(pk_list), sql.constants.GET_ITERATOR_CHUNK_SIZE): 
     156                    query = sql.UpdateQuery(model, connection) 
     157                    query.where = query.where_class() 
     158                    query.where.add((sql.where.Constraint(None, pk_field.column, pk_field), 'in', pk_list), sql.where.AND) 
     159                    query.add_update_values({field.name: value}) 
     160                    query.execute_sql() 
     161                 
     162    def update_instances(self): 
     163        for model, instances_for_fieldvalues in self.data.iteritems(): 
     164            for (field, value), instances in instances_for_fieldvalues.iteritems(): 
     165                for obj in instances: 
     166                    setattr(obj, field.attname, value) 
     167 
     168    def __nonzero__(self): 
     169        return bool(self.data) 
     170     
     171    # FIXME: used by the tests, but not really needed: 
     172    def keys(self): 
     173        return self.data.keys() 
     174 
     175 
     176 
    127177class QueryWrapper(object): 
    128178    """ 
    129179    A type that indicates the contents are an SQL fragment and the associate