Django

Code

Ticket #7539: on_delete_on_update-r11620.diff

File on_delete_on_update-r11620.diff, 21.0 kB (added by glassfordm, 5 months ago)

Same as previous patch, but with correct extension

  • django/db/models/base.py

    old new  
    1313from django.db.models.fields import AutoField, FieldDoesNotExist 
    1414from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField 
    1515from django.db.models.query import delete_objects, Q 
    16 from django.db.models.query_utils import CollectedObjects, DeferredAttribute 
     16from django.db.models.query_utils import CollectedFields, CollectedObjects, DeferredAttribute 
    1717from django.db.models.options import Options 
    18 from django.db import connection, transaction, DatabaseError 
     18from django.db import connection, transaction, DatabaseError, IntegrityError 
    1919from django.db.models import signals 
     20from django.db.models.fields.related import CASCADE, PROTECT, SET_NULL, SET_DEFAULT 
    2021from django.db.models.loading import register_models, get_model 
    2122from django.utils.functional import curry 
    2223from django.utils.encoding import smart_str, force_unicode, smart_unicode 
     
    507508 
    508509    save_base.alters_data = True 
    509510 
    510     def _collect_sub_objects(self, seen_objs, parent=None, nullable=False): 
     511    def _collect_sub_objects(self, seen_objs, fields_to_set, parent=None, nullable=False): 
    511512        """ 
    512513        Recursively populates seen_objs with all objects related to this 
    513514        object. 
     
    519520        pk_val = self._get_pk_val() 
    520521        if seen_objs.add(self.__class__, pk_val, self, parent, nullable): 
    521522            return 
     523             
     524        def _handle_sub_obj(related, sub_obj): 
     525            on_delete = related.field.rel.on_delete 
     526            if on_delete is None: 
     527                #If no explicit on_delete option is specified, use the old 
     528                #django behavior as the default: SET_NULL if the foreign 
     529                #key is nullable, otherwise CASCADE. 
     530                if related.field.null: 
     531                    on_delete = SET_NULL 
     532                else: 
     533                    on_delete = CASCADE 
     534                 
     535            if on_delete == CASCADE: 
     536                sub_obj._collect_sub_objects(seen_objs, fields_to_set, self.__class__) 
     537            elif on_delete == PROTECT: 
     538                msg = '[Django] Cannot delete a parent object: a foreign key constraint fails (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % ( 
     539                    sub_obj.__class__, 
     540                    sub_obj._get_pk_val(), 
     541                    self.__class__, 
     542                    pk_val, 
     543                    ) 
     544                raise IntegrityError(msg) 
     545            elif on_delete == SET_NULL: 
     546                if not related.field.null: 
     547                    msg = '[Django] Cannot delete a parent object: foreign key constraint on_delete=SET_NULL is specified for a non-nullable foreign key (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % ( 
     548                        sub_obj.__class__, 
     549                        sub_obj._get_pk_val(), 
     550                        self.__class__, 
     551                        pk_val, 
     552                        ) 
     553                    raise IntegrityError(msg) 
     554                fields_to_set.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name, None) 
     555            elif on_delete == SET_DEFAULT: 
     556                if not related.field.has_default(): 
     557                    msg = '[Django] Cannot delete a parent object: foreign key constraint on_delete=SET_DEFAULT is specified for a foreign key with no default value (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % ( 
     558                        sub_obj.__class__, 
     559                        sub_obj._get_pk_val(), 
     560                        self.__class__, 
     561                        pk_val, 
     562                        ) 
     563                    raise IntegrityError(msg) 
     564                fields_to_set.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name, related.field.get_default()) 
     565            else: 
     566                raise AttributeError('Unexpected value for on_delete') 
    522567 
    523568        for related in self._meta.get_all_related_objects(): 
    524569            rel_opts_name = related.get_accessor_name() 
    525570            if isinstance(related.field.rel, OneToOneRel): 
    526571                try: 
     572                    # delattr(self, rel_opts_name) #Delete first to clear any stale cache  
     573                        #TODO: the above line is a bit of a hack 
     574                        #It's one way (not a very good one) to work around stale cache data causing 
     575                        #spurious RESTRICT errors, etc; it would be better to prevent the cache from 
     576                        #becoming stale in the first place. 
    527577                    sub_obj = getattr(self, rel_opts_name) 
    528578                except ObjectDoesNotExist: 
    529579                    pass 
    530580                else: 
    531                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null
     581                    _handle_sub_obj(related, sub_obj
    532582            else: 
    533583                # To make sure we can access all elements, we can't use the 
    534584                # normal manager on the related object. So we work directly 
     
    541591                    raise AssertionError("Should never get here.") 
    542592                delete_qs = rel_descriptor.delete_manager(self).all() 
    543593                for sub_obj in delete_qs: 
    544                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null
     594                    _handle_sub_obj(related, sub_obj
    545595 
    546596        # Handle any ancestors (for the model-inheritance case). We do this by 
    547597        # traversing to the most remote parent classes -- those with no parents 
     
    556606                continue 
    557607            # At this point, parent_obj is base class (no ancestor models). So 
    558608            # delete it and all its descendents. 
    559             parent_obj._collect_sub_objects(seen_objs
     609            parent_obj._collect_sub_objects(seen_objs, fields_to_set
    560610 
    561611    def delete(self): 
    562612        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) 
    563613 
    564614        # Find all the objects than need to be deleted. 
    565615        seen_objs = CollectedObjects() 
    566         self._collect_sub_objects(seen_objs) 
     616        fields_to_set = CollectedFields() 
     617        self._collect_sub_objects(seen_objs, fields_to_set) 
    567618 
    568619        # Actually delete the objects. 
    569         delete_objects(seen_objs) 
    570  
     620        delete_objects(seen_objs, fields_to_set) 
    571621    delete.alters_data = True 
    572622 
    573623    def _get_FIELD_display(self, field): 
  • 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 
     31ALLOWED_ON_DELETE_ACTION_TYPES = set([None, CASCADE, PROTECT, SET_NULL, SET_DEFAULT]) 
     32 
    2333def add_lazy_relation(cls, field, relation, operation): 
    2434    """ 
    2535    Adds a lookup on ``cls`` when a related field is defined using a string, 
     
    218228        # object you just set. 
    219229        setattr(instance, self.cache_name, value) 
    220230        setattr(value, self.related.field.get_cache_name(), instance) 
     231         
     232    #TODO: the following function is a bit of a hack 
     233        #It's one way (not a very good one) to work around stale cache data causing 
     234        #spurious RESTRICT errors, etc; it would be better to prevent the cache from 
     235        #becoming stale in the first place. 
     236    # def __delete__(self, instance): 
     237    #     try: 
     238    #         return delattr(instance, self.cache_name) 
     239    #     except AttributeError: 
     240    #         pass 
    221241 
    222242class ReverseSingleRelatedObjectDescriptor(object): 
    223243    # This class provides the functionality that makes the related-object 
     
    628648 
    629649class ManyToOneRel(object): 
    630650    def __init__(self, to, field_name, related_name=None, 
    631             limit_choices_to=None, lookup_overrides=None, parent_link=False): 
     651            limit_choices_to=None, lookup_overrides=None, parent_link=False, 
     652            on_delete=None): 
    632653        try: 
    633654            to._meta 
    634655        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT 
     
    641662        self.lookup_overrides = lookup_overrides or {} 
    642663        self.multiple = True 
    643664        self.parent_link = parent_link 
     665        self.on_delete = on_delete 
    644666 
    645667    def get_related_field(self): 
    646668        """ 
     
    655677 
    656678class OneToOneRel(ManyToOneRel): 
    657679    def __init__(self, to, field_name, related_name=None, 
    658             limit_choices_to=None, lookup_overrides=None, parent_link=False): 
     680            limit_choices_to=None, lookup_overrides=None, parent_link=False,  
     681            on_delete=None): 
    659682        super(OneToOneRel, self).__init__(to, field_name, 
    660683                related_name=related_name, limit_choices_to=limit_choices_to, 
    661                 lookup_overrides=lookup_overrides, parent_link=parent_link) 
     684                lookup_overrides=lookup_overrides, parent_link=parent_link, 
     685                on_delete=on_delete) 
    662686        self.multiple = False 
    663687 
    664688class ManyToManyRel(object): 
     
    697721            related_name=kwargs.pop('related_name', None), 
    698722            limit_choices_to=kwargs.pop('limit_choices_to', None), 
    699723            lookup_overrides=kwargs.pop('lookup_overrides', None), 
    700             parent_link=kwargs.pop('parent_link', False)) 
     724            parent_link=kwargs.pop('parent_link', False), 
     725            on_delete=kwargs.pop('on_delete', None)) 
    701726        Field.__init__(self, **kwargs) 
    702727 
    703728        self.db_index = True 
     
    742767            target = self.rel.to._meta.db_table 
    743768        cls._meta.duplicate_targets[self.column] = (target, "o2m") 
    744769 
     770        on_delete = self.rel.on_delete 
     771        if on_delete not in ALLOWED_ON_DELETE_ACTION_TYPES: 
     772            raise ValueError("Invalid value 'on_delete=%s' specified for %s %s.%s." % (on_delete, type(self).__name__, cls.__name__, name)) 
     773        if on_delete == SET_NULL and not self.null: 
     774            specification = "'on_delete=SET_NULL'" 
     775            raise ValueError("%s specified for %s '%s.%s', but the field is not nullable." % (specification, type(self).__name__, cls.__name__, name)) 
     776        if on_delete == SET_DEFAULT and not self.has_default(): 
     777            specification = "'on_delete=SET_DEFAULT'" 
     778            raise ValueError("%s specified for %s '%s.%s', but the field has no default value." % (specification, type(self).__name__, cls.__name__, name)) 
     779 
    745780    def contribute_to_related_class(self, cls, related): 
    746781        setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) 
    747782 
  • 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  
    1111 
    1212from django.db import connection, transaction, IntegrityError 
    1313from django.db.models.aggregates import Aggregate 
     14from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE 
    1415from django.db.models.fields import DateField 
    15 from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory 
     16from django.db.models.query_utils import Q, select_related_descend, CollectedFields, CollectedObjects, CyclicDependency, deferred_class_factory 
    1617from django.db.models import signals, sql 
    1718 
    1819 
     
    391392            # Collect all the objects to be deleted in this chunk, and all the 
    392393            # objects that are related to the objects that are to be deleted. 
    393394            seen_objs = CollectedObjects(seen_objs) 
     395            fields_to_set = CollectedFields() 
    394396            for object in del_query[:CHUNK_SIZE]: 
    395                 object._collect_sub_objects(seen_objs
     397                object._collect_sub_objects(seen_objs, fields_to_set
    396398 
    397399            if not seen_objs: 
    398400                break 
    399             delete_objects(seen_objs
     401            delete_objects(seen_objs, fields_to_set
    400402 
    401403        # Clear the result cache, in case this QuerySet gets reused. 
    402404        self._result_cache = None 
     
    10021004                setattr(obj, f.get_cache_name(), rel_obj) 
    10031005    return obj, index_end 
    10041006 
    1005 def delete_objects(seen_objs): 
     1007def delete_objects(seen_objs, fields_to_set): 
    10061008    """ 
    10071009    Iterate through a list of seen classes, and remove any instances that are 
    10081010    referred to. 
     
    10231025 
    10241026    obj_pairs = {} 
    10251027    try: 
     1028        for cls, cls_dct in fields_to_set.iteritems(): 
     1029            #TODO: batch these, similar to UpdateQuery.clear_related? 
     1030            #(Note that it may be harder to do here because the default value 
     1031            #for a given field may be different for each instance, 
     1032            #while UpdateQuery.clear_related always uses the value None). 
     1033            query = sql.UpdateQuery(cls, connection) 
     1034            for instance, field_names_and_values in cls_dct.itervalues(): 
     1035                query.where = query.where_class() 
     1036                pk = query.model._meta.pk 
     1037                query.where.add((sql.where.Constraint(None, pk.column, pk), 'exact', instance.pk), sql.where.AND) 
     1038                query.add_update_values(field_names_and_values) 
     1039                query.execute_sql() 
     1040                     
    10261041        for cls in ordered_classes: 
    10271042            items = seen_objs[cls].items() 
    10281043            items.sort() 
     
    10321047            for pk_val, instance in items: 
    10331048                signals.pre_delete.send(sender=cls, instance=instance) 
    10341049 
     1050            # Handle related GenericRelation and ManyToManyField instances 
    10351051            pk_list = [pk for pk,instance in items] 
    10361052            del_query = sql.DeleteQuery(cls, connection) 
    10371053            del_query.delete_batch_related(pk_list) 
    10381054 
    1039             update_query = sql.UpdateQuery(cls, connection) 
    1040             for field, model in cls._meta.get_fields_with_model(): 
    1041                 if (field.rel and field.null and field.rel.to in seen_objs and 
    1042                         filter(lambda f: f.column == field.rel.get_related_field().column, 
    1043                         field.rel.to._meta.fields)): 
    1044                     if model: 
    1045                         sql.UpdateQuery(model, connection).clear_related(field, 
    1046                                 pk_list) 
    1047                     else: 
    1048                         update_query.clear_related(field, pk_list) 
    1049  
    1050         # Now delete the actual data. 
    10511055        for cls in ordered_classes: 
    10521056            items = obj_pairs[cls] 
    10531057            items.reverse() 
    1054  
    10551058            pk_list = [pk for pk,instance in items] 
    10561059            del_query = sql.DeleteQuery(cls, connection) 
    10571060            del_query.delete_batch(pk_list) 
    10581061 
    1059             # Last cleanup; set NULLs where there once was a reference to the 
    1060             # object, NULL the primary key of the found objects, and perform 
    1061             # post-notification. 
     1062        #Last cleanup; set NULLs and default values where there once was a  
     1063        #reference to the object, NULL the primary key of the found objects,  
     1064        #and perform post-notification. 
     1065        for cls, cls_dct in fields_to_set.iteritems(): 
     1066            for instance, field_names_and_values in cls_dct.itervalues(): 
     1067                for field_name, field_value in field_names_and_values.iteritems(): 
     1068                    field = cls._meta.get_field_by_name(field_name)[0] 
     1069                    setattr(instance, field.attname, field_value) 
     1070        for cls in ordered_classes: 
     1071            items = obj_pairs[cls] 
     1072            items.reverse() 
    10621073            for pk_val, instance in items: 
    10631074                for field in cls._meta.fields: 
    10641075                    if field.rel and field.null and field.rel.to in seen_objs: 
  • 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 name 
     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        self.data = {} 
     136 
     137    def add(self, model, pk, obj, field_name, field_value): 
     138        """ 
     139        Adds an item. 
     140        model is the class of the object being added, 
     141        pk is the primary key, obj is the object itself,  
     142        field_name is the name of the field to be set, 
     143        field_value is the value it needs to be set to. 
     144        """ 
     145        d = self.data.setdefault(model, SortedDict()) 
     146        obj, field_names_and_values = d.setdefault(pk, (obj, dict())) 
     147        assert field_name not in field_names_and_values or field_names_and_values[field_name] == field_value 
     148        field_names_and_values[field_name] = field_value 
     149 
     150    def __contains__(self, key): 
     151        return self.data.__contains__(key) 
     152 
     153    def __getitem__(self, key): 
     154        return self.data[key] 
     155 
     156    def __nonzero__(self): 
     157        return bool(self.data) 
     158 
     159    def iteritems(self): 
     160        return self.data.iteritems() 
     161 
     162    def iterkeys(self): 
     163        return self.data.iterkeys() 
     164 
     165    def itervalues(self): 
     166        return self.data.itervalues() 
     167 
     168    def items(self): 
     169        return self.data.items() 
     170 
     171    def keys(self): 
     172        return self.data.keys() 
     173 
     174    def values(self): 
     175        return self.data.values() 
     176 
    127177class QueryWrapper(object): 
    128178    """ 
    129179    A type that indicates the contents are an SQL fragment and the associate 
  • 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}