Ticket #7539: 7539.on_delete.diff

File 7539.on_delete.diff, 18.2 KB (added by Johannes Dollinger, 14 years ago)
  • 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}
  • django/db/models/base.py

     
    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

     
    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

     
    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

     
    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

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