Ticket #7539: on_delete_on_update-r11620.diff

File on_delete_on_update-r11620.diff, 21.0 KB (added by glassfordm, 14 years ago)

Same as previous patch, but with correct extension

  • django/db/models/base.py

    MJG-MBP:django_on_delete_patch mjg$ svn diff
     
    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

     
    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

     
    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

     
    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

     
    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

     
    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}
Back to Top