Ticket #7539: 7539.on_delete.r14218.diff

File 7539.on_delete.r14218.diff, 47.6 KB (added by Johannes Dollinger, 14 years ago)
  • django/db/models/sql/subqueries.py

     
    2626        self.where = where
    2727        self.get_compiler(using).execute_sql(None)
    2828
    29     def delete_batch(self, pk_list, using):
     29    def delete_batch(self, pk_list, using, field=None):
    3030        """
    3131        Set up and execute delete queries for all the objects in pk_list.
    3232
    3333        More than one physical query may be executed if there are a
    3434        lot of values in pk_list.
    3535        """
     36        if not field:
     37            field = self.model._meta.pk
    3638        for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
    3739            where = self.where_class()
    38             field = self.model._meta.pk
    3940            where.add((Constraint(None, field.column, field), 'in',
    4041                    pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND)
    4142            self.do_query(self.model._meta.db_table, where, using=using)
     
    6768                related_updates=self.related_updates.copy(), **kwargs)
    6869
    6970
    70     def clear_related(self, related_field, pk_list, using):
    71         """
    72         Set up and execute an update query that clears related entries for the
    73         keys in pk_list.
    74 
    75         This is used by the QuerySet.delete_objects() method.
    76         """
     71    def update_batch(self, pk_list, values, using):
     72        pk_field = self.model._meta.pk
     73        self.add_update_values(values)
    7774        for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
    7875            self.where = self.where_class()
    79             f = self.model._meta.pk
    80             self.where.add((Constraint(None, f.column, f), 'in',
     76            self.where.add((Constraint(None, pk_field.column, pk_field), 'in',
    8177                    pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]),
    8278                    AND)
    83             self.values = [(related_field, None, None)]
    8479            self.get_compiler(using).execute_sql(None)
    8580
    8681    def add_update_values(self, values):
  • django/db/models/base.py

     
    77from django.db.models.fields import AutoField, FieldDoesNotExist
    88from django.db.models.fields.related import (OneToOneRel, ManyToOneRel,
    99    OneToOneField, add_lazy_relation)
    10 from django.db.models.query import delete_objects, Q
    11 from django.db.models.query_utils import CollectedObjects, DeferredAttribute
     10from django.db.models.query import Q
     11from django.db.models.query_utils import DeferredAttribute
     12from django.db.models.deletion import Collector
    1213from django.db.models.options import Options
    1314from django.db import connections, router, transaction, DatabaseError, DEFAULT_DB_ALIAS
    1415from django.db.models import signals
     
    561562
    562563    save_base.alters_data = True
    563564
    564     def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
    565         """
    566         Recursively populates seen_objs with all objects related to this
    567         object.
    568 
    569         When done, seen_objs.items() will be in the format:
    570             [(model_class, {pk_val: obj, pk_val: obj, ...}),
    571              (model_class, {pk_val: obj, pk_val: obj, ...}), ...]
    572         """
    573         pk_val = self._get_pk_val()
    574         if seen_objs.add(self.__class__, pk_val, self,
    575                          type(parent), parent, nullable):
    576             return
    577 
    578         for related in self._meta.get_all_related_objects():
    579             rel_opts_name = related.get_accessor_name()
    580             if not related.field.rel.multiple:
    581                 try:
    582                     sub_obj = getattr(self, rel_opts_name)
    583                 except ObjectDoesNotExist:
    584                     pass
    585                 else:
    586                     sub_obj._collect_sub_objects(seen_objs, self, related.field.null)
    587             else:
    588                 # To make sure we can access all elements, we can't use the
    589                 # normal manager on the related object. So we work directly
    590                 # with the descriptor object.
    591                 for cls in self.__class__.mro():
    592                     if rel_opts_name in cls.__dict__:
    593                         rel_descriptor = cls.__dict__[rel_opts_name]
    594                         break
    595                 else:
    596                     # in the case of a hidden fkey just skip it, it'll get
    597                     # processed as an m2m
    598                     if not related.field.rel.is_hidden():
    599                         raise AssertionError("Should never get here.")
    600                     else:
    601                         continue
    602                 delete_qs = rel_descriptor.delete_manager(self).all()
    603                 for sub_obj in delete_qs:
    604                     sub_obj._collect_sub_objects(seen_objs, self, related.field.null)
    605 
    606         for related in self._meta.get_all_related_many_to_many_objects():
    607             if related.field.rel.through:
    608                 db = router.db_for_write(related.field.rel.through.__class__, instance=self)
    609                 opts = related.field.rel.through._meta
    610                 reverse_field_name = related.field.m2m_reverse_field_name()
    611                 nullable = opts.get_field(reverse_field_name).null
    612                 filters = {reverse_field_name: self}
    613                 for sub_obj in related.field.rel.through._base_manager.using(db).filter(**filters):
    614                     sub_obj._collect_sub_objects(seen_objs, self, nullable)
    615 
    616         for f in self._meta.many_to_many:
    617             if f.rel.through:
    618                 db = router.db_for_write(f.rel.through.__class__, instance=self)
    619                 opts = f.rel.through._meta
    620                 field_name = f.m2m_field_name()
    621                 nullable = opts.get_field(field_name).null
    622                 filters = {field_name: self}
    623                 for sub_obj in f.rel.through._base_manager.using(db).filter(**filters):
    624                     sub_obj._collect_sub_objects(seen_objs, self, nullable)
    625             else:
    626                 # m2m-ish but with no through table? GenericRelation: cascade delete
    627                 for sub_obj in f.value_from_object(self).all():
    628                     # Generic relations not enforced by db constraints, thus we can set
    629                     # nullable=True, order does not matter
    630                     sub_obj._collect_sub_objects(seen_objs, self, True)
    631 
    632         # Handle any ancestors (for the model-inheritance case). We do this by
    633         # traversing to the most remote parent classes -- those with no parents
    634         # themselves -- and then adding those instances to the collection. That
    635         # will include all the child instances down to "self".
    636         parent_stack = [p for p in self._meta.parents.values() if p is not None]
    637         while parent_stack:
    638             link = parent_stack.pop()
    639             parent_obj = getattr(self, link.name)
    640             if parent_obj._meta.parents:
    641                 parent_stack.extend(parent_obj._meta.parents.values())
    642                 continue
    643             # At this point, parent_obj is base class (no ancestor models). So
    644             # delete it and all its descendents.
    645             parent_obj._collect_sub_objects(seen_objs)
    646 
    647565    def delete(self, using=None):
    648566        using = using or router.db_for_write(self.__class__, instance=self)
    649567        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)
    650568
    651         # Find all the objects than need to be deleted.
    652         seen_objs = CollectedObjects()
    653         self._collect_sub_objects(seen_objs)
     569        collector = Collector()
     570        collector.collect([self], using=using)
     571        collector.delete(using=using)
    654572
    655         # Actually delete the objects.
    656         delete_objects(seen_objs, using)
    657 
    658573    delete.alters_data = True
    659574
    660575    def _get_FIELD_display(self, field):
  • django/db/models/options.py

     
    370370                    cache[obj] = parent
    371371                else:
    372372                    cache[obj] = model
    373         for klass in get_models():
     373        for klass in get_models(include_auto_created=True):
    374374            for f in klass._meta.local_fields:
    375375                if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
    376376                    cache[RelatedObject(f.rel.to, klass, f)] = None
  • django/db/models/fields/related.py

     
    77from django.db.models.related import RelatedObject
    88from django.db.models.query import QuerySet
    99from django.db.models.query_utils import QueryWrapper
     10from django.db.models.deletion import CASCADE
    1011from django.utils.encoding import smart_unicode
    1112from django.utils.translation import ugettext_lazy as _, string_concat, ungettext, ugettext
    1213from django.utils.functional import curry
     
    733734        manager.add(*value)
    734735
    735736class ManyToOneRel(object):
    736     def __init__(self, to, field_name, related_name=None,
    737             limit_choices_to=None, lookup_overrides=None, parent_link=False):
     737    def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None):
    738738        try:
    739739            to._meta
    740740        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
     
    744744        if limit_choices_to is None:
    745745            limit_choices_to = {}
    746746        self.limit_choices_to = limit_choices_to
    747         self.lookup_overrides = lookup_overrides or {}
    748747        self.multiple = True
    749748        self.parent_link = parent_link
     749        self.on_delete = on_delete
    750750
    751751    def is_hidden(self):
    752752        "Should the related object be hidden?"
     
    764764        return data[0]
    765765
    766766class OneToOneRel(ManyToOneRel):
    767     def __init__(self, to, field_name, related_name=None,
    768             limit_choices_to=None, lookup_overrides=None, parent_link=False):
     767    def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None):
    769768        super(OneToOneRel, self).__init__(to, field_name,
    770769                related_name=related_name, limit_choices_to=limit_choices_to,
    771                 lookup_overrides=lookup_overrides, parent_link=parent_link)
     770                parent_link=parent_link, on_delete=on_delete
     771        )
    772772        self.multiple = False
    773773
    774774class ManyToManyRel(object):
     
    820820        kwargs['rel'] = rel_class(to, to_field,
    821821            related_name=kwargs.pop('related_name', None),
    822822            limit_choices_to=kwargs.pop('limit_choices_to', None),
    823             lookup_overrides=kwargs.pop('lookup_overrides', None),
    824             parent_link=kwargs.pop('parent_link', False))
     823            parent_link=kwargs.pop('parent_link', False),
     824            on_delete=kwargs.pop('on_delete', CASCADE),
     825        )
    825826        Field.__init__(self, **kwargs)
    826827
    827828    def validate(self, value, model_instance):
  • 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.deletion import CASCADE, PROTECT, SET, SET_NULL, SET_DEFAULT, DO_NOTHING
    1415from django.db.models import signals
    1516
    1617# Admin stages.
  • django/db/models/deletion.py

     
     1from django.utils.datastructures import SortedDict
     2from django.utils.functional import wraps
     3from django.db import connections, transaction, IntegrityError
     4from django.db.models import signals, sql
     5from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
     6
     7def CASCADE(collector, field, sub_objs):
     8    collector.collect(sub_objs, field.rel.to, field.null)
     9    if field.null:
     10        # FIXME: there should be a connection feature indicating whether nullable related fields should be nulled out before deletion
     11        collector.add_field_update(field, None, sub_objs)
     12
     13def PROTECT(collector, field, sub_objs):
     14    msg = "Cannot delete some instances of model '%s' because they are referenced through a protected foreign key: '%s.%s'" % (
     15        field.rel.to.__name__, sub_objs[0].__class__.__name__, field.name
     16    )
     17    raise IntegrityError(msg)
     18
     19def SET(value):
     20    def set_on_delete(collector, field, sub_objs):
     21        collector.add_field_update(field, value, sub_objs)
     22    return set_on_delete
     23
     24def SET_NULL(collector, field, sub_objs):
     25    collector.add_field_update(field, None, sub_objs)
     26
     27def SET_DEFAULT(collector, field, sub_objs):
     28    collector.add_field_update(field, field.get_default(), sub_objs)
     29
     30def DO_NOTHING(collector, field, sub_objs):
     31    pass
     32
     33def force_managed(func):
     34    @wraps(func)
     35    def decorated(*args, **kwargs):
     36        if not transaction.is_managed():
     37            transaction.enter_transaction_management()
     38            forced_managed = True
     39        else:
     40            forced_managed = False
     41        try:                   
     42            func(*args, **kwargs)
     43            if forced_managed:
     44                transaction.commit()
     45            else:
     46                transaction.commit_unless_managed()
     47        finally:
     48            if forced_managed:
     49                transaction.leave_transaction_management()
     50    return decorated
     51
     52class Collector(object):
     53    def __init__(self):
     54        self.data = {} # {model: [instances]}
     55        self.batches = {} # {model: {field: set([instances])}}
     56        self.field_updates = {} # {model: {(field, value): set([instances])}}       
     57        self.dependencies = {} # {model: set([models])}
     58
     59    def add(self, objs, source=None, nullable=False):
     60        """
     61        Adds 'objs' to the collection of objects to be deleted.
     62        If the call is the result of a cascade, 'source' should be the model that caused it
     63        and 'nullable' should be set to True, if the relation can be null.
     64       
     65        Returns a list of all objects that were not already collected.
     66        """
     67        if not objs:
     68            return []
     69        new_objs = []
     70        model = objs[0].__class__
     71        instances = self.data.setdefault(model, [])
     72        for obj in objs:
     73            if obj not in instances:
     74                new_objs.append(obj)
     75        instances.extend(new_objs)
     76        # Nullable relationships can be ignored -- they are nulled out before
     77        # deleting, and therefore do not affect the order in which objects
     78        # have to be deleted.
     79        if new_objs and source is not None and not nullable:
     80            self.dependencies.setdefault(source, set()).add(model)
     81        return new_objs
     82       
     83    def add_batch(self, model, field, objs):
     84        """
     85        Schedules a batch delete. Every instance of 'model' that is related to an instance of 'obj' through 'field' will be deleted.
     86        """
     87        self.batches.setdefault(model, {}).setdefault(field, set()).update(objs)
     88       
     89    def add_field_update(self, field, value, objs):
     90        """
     91        Schedules a field update. 'objs' must be a homogenous iterable collection of model instances (e.g. a QuerySet).
     92        """
     93        objs = list(objs)
     94        if not objs:
     95            return
     96        model = objs[0].__class__
     97        self.field_updates.setdefault(model, {}).setdefault((field, value), set()).update(objs)
     98       
     99    def collect(self, objs, source=None, nullable=False, collect_related=True, using=None):
     100        """
     101        Adds 'objs' to the collection of objects to be deleted as well as all parent instances.
     102        'objs' must be a homogenous iterable collection of model instances (e.g. a QuerySet).
     103        If 'collect_related' is True, related objects will be handled by their respective on_delete handler.
     104       
     105        If the call is the result of a cascade, 'source' should be the model that caused it
     106        and 'nullable' should be set to True, if the relation can be null.
     107        """
     108        new_objs = self.add(objs, source, nullable)
     109        if not new_objs:
     110            return
     111        model = new_objs[0].__class__
     112       
     113        # Recusively collect parent models, but not their related objects.
     114        for parent, ptr in model._meta.parents.items():
     115            if ptr:
     116                parent_objs = [getattr(obj, ptr.name) for obj in new_objs]
     117                self.collect(parent_objs, model, collect_related=False)
     118
     119        if collect_related:
     120            for related in model._meta.get_all_related_objects():
     121                field = related.field
     122                if field.rel.is_hidden():
     123                    self.add_batch(related.model, field, new_objs)
     124                else:
     125                    sub_objs = related.model._base_manager.using(using).filter(**{"%s__in" % field.name: new_objs})
     126                    if not sub_objs:
     127                        continue
     128                    field.rel.on_delete(self, field, sub_objs)
     129
     130            # FIXME: support for generic relations should not require special handling
     131            for field in model._meta.many_to_many:
     132                if not field.rel.through:
     133                    # m2m-ish but with no through table? GenericRelation: cascade delete
     134                    for obj in new_objs:
     135                        self.collect(field.value_from_object(obj).all(), model, nullable=True, using=using)
     136
     137    def instances_with_model(self):
     138        for model, instances in self.data.iteritems():
     139            for obj in instances:
     140                yield model, obj
     141               
     142    def __iter__(self):
     143        for model, obj in self.instances_with_model():
     144            yield obj
     145        for model, batches in self.batches.iteritems():
     146            for field, instances in batches.iteritems():
     147                for obj in model._default_manager.filter(**{'%s__in' % field.name: instances}):
     148                    yield obj
     149
     150    def __nonzero__(self):
     151        return bool(self.data)
     152
     153    def sort(self):
     154        sorted_models = []
     155        models = self.data.keys()
     156        while len(sorted_models) < len(models):
     157            found = False
     158            for model in models:
     159                if model in sorted_models:
     160                    continue
     161                dependencies = self.dependencies.get(model)
     162                if not dependencies or not dependencies.difference(sorted_models):
     163                    sorted_models.append(model)
     164                    found = True
     165            if not found:
     166                return
     167        self.data = SortedDict([(model, self.data[model]) for model in sorted_models])
     168   
     169    @force_managed
     170    def delete(self, using=None):
     171        # sort instance collections
     172        for instances in self.data.itervalues():
     173            instances.sort(key=lambda obj: obj.pk)
     174
     175        # if possible, bring the models in an order suitable for databases that don't support transactions
     176        # or cannot defer contraint checks until the end of a transaction.
     177        self.sort()
     178       
     179        # send pre_delete signals
     180        for model, obj in self.instances_with_model():
     181            if not model._meta.auto_created:
     182                signals.pre_delete.send(sender=model, instance=obj, using=using)
     183
     184        # update fields
     185        for model, instances_for_fieldvalues in self.field_updates.iteritems():
     186            query = sql.UpdateQuery(model)
     187            for (field, value), instances in instances_for_fieldvalues.iteritems():
     188                query.update_batch([obj.pk for obj in instances], {field.name: value}, using)
     189
     190        # reverse instance collections
     191        for instances in self.data.itervalues():
     192            instances.reverse()
     193
     194        # delete batches
     195        for model, batches in self.batches.iteritems():
     196            query = sql.DeleteQuery(model)
     197            for field, instances in batches.iteritems():
     198                query.delete_batch([obj.pk for obj in instances], using, field)
     199
     200        # delete instances
     201        for model, instances in self.data.iteritems():
     202            query = sql.DeleteQuery(model)
     203            pk_list = [obj.pk for obj in instances]
     204            #query.delete_generic_relation_hack(pk_list, using)
     205            query.delete_batch(pk_list, using)
     206       
     207        # send post_delete signals
     208        for model, obj in self.instances_with_model():
     209            if not model._meta.auto_created:
     210                signals.post_delete.send(sender=model, instance=obj, using=using)
     211       
     212        # update collected instances
     213        for model, instances_for_fieldvalues in self.field_updates.iteritems():
     214            for (field, value), instances in instances_for_fieldvalues.iteritems():
     215                for obj in instances:
     216                    setattr(obj, field.attname, value)
     217        for model, instances in self.data.iteritems():
     218            for instance in instances:
     219                setattr(instance, model._meta.pk.attname, None)
  • django/db/models/query.py

     
    77from django.db import connections, router, transaction, IntegrityError
    88from django.db.models.aggregates import Aggregate
    99from django.db.models.fields import DateField
    10 from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory, InvalidQuery
     10from django.db.models.query_utils import Q, select_related_descend, deferred_class_factory, InvalidQuery
     11from django.db.models.deletion import Collector
    1112from django.db.models import signals, sql
    1213from django.utils.copycompat import deepcopy
    1314
     
    426427        del_query.query.select_related = False
    427428        del_query.query.clear_ordering()
    428429
    429         # Delete objects in chunks to prevent the list of related objects from
    430         # becoming too long.
    431         seen_objs = None
    432         del_itr = iter(del_query)
    433         while 1:
    434             # Collect a chunk of objects to be deleted, and then all the
    435             # objects that are related to the objects that are to be deleted.
    436             # The chunking *isn't* done by slicing the del_query because we
    437             # need to maintain the query cache on del_query (see #12328)
    438             seen_objs = CollectedObjects(seen_objs)
    439             for i, obj in izip(xrange(CHUNK_SIZE), del_itr):
    440                 obj._collect_sub_objects(seen_objs)
     430        collector = Collector()
     431        collector.collect(del_query)
     432        collector.delete(using=del_query.db)
    441433
    442             if not seen_objs:
    443                 break
    444             delete_objects(seen_objs, del_query.db)
    445 
    446434        # Clear the result cache, in case this QuerySet gets reused.
    447435        self._result_cache = None
    448436    delete.alters_data = True
  • django/db/models/query_utils.py

     
    1414from django.utils.datastructures import SortedDict
    1515
    1616
    17 class CyclicDependency(Exception):
    18     """
    19     An error when dealing with a collection of objects that have a cyclic
    20     dependency, i.e. when deleting multiple objects.
    21     """
    22     pass
    23 
    2417class InvalidQuery(Exception):
    2518    """
    2619    The query passed to raw isn't a safe query to use with raw.
     
    2821    pass
    2922
    3023
    31 class CollectedObjects(object):
    32     """
    33     A container that stores keys and lists of values along with remembering the
    34     parent objects for all the keys.
    35 
    36     This is used for the database object deletion routines so that we can
    37     calculate the 'leaf' objects which should be deleted first.
    38 
    39     previously_seen is an optional argument. It must be a CollectedObjects
    40     instance itself; any previously_seen collected object will be blocked from
    41     being added to this instance.
    42     """
    43 
    44     def __init__(self, previously_seen=None):
    45         self.data = {}
    46         self.children = {}
    47         if previously_seen:
    48             self.blocked = previously_seen.blocked
    49             for cls, seen in previously_seen.data.items():
    50                 self.blocked.setdefault(cls, SortedDict()).update(seen)
    51         else:
    52             self.blocked = {}
    53 
    54     def add(self, model, pk, obj, parent_model, parent_obj=None, nullable=False):
    55         """
    56         Adds an item to the container.
    57 
    58         Arguments:
    59         * model - the class of the object being added.
    60         * pk - the primary key.
    61         * obj - the object itself.
    62         * parent_model - the model of the parent object that this object was
    63           reached through.
    64         * parent_obj - the parent object this object was reached
    65           through (not used here, but needed in the API for use elsewhere)
    66         * nullable - should be True if this relation is nullable.
    67 
    68         Returns True if the item already existed in the structure and
    69         False otherwise.
    70         """
    71         if pk in self.blocked.get(model, {}):
    72             return True
    73 
    74         d = self.data.setdefault(model, SortedDict())
    75         retval = pk in d
    76         d[pk] = obj
    77         # Nullable relationships can be ignored -- they are nulled out before
    78         # deleting, and therefore do not affect the order in which objects
    79         # have to be deleted.
    80         if parent_model is not None and not nullable:
    81             self.children.setdefault(parent_model, []).append(model)
    82         return retval
    83 
    84     def __contains__(self, key):
    85         return self.data.__contains__(key)
    86 
    87     def __getitem__(self, key):
    88         return self.data[key]
    89 
    90     def __nonzero__(self):
    91         return bool(self.data)
    92 
    93     def iteritems(self):
    94         for k in self.ordered_keys():
    95             yield k, self[k]
    96 
    97     def items(self):
    98         return list(self.iteritems())
    99 
    100     def keys(self):
    101         return self.ordered_keys()
    102 
    103     def ordered_keys(self):
    104         """
    105         Returns the models in the order that they should be dealt with (i.e.
    106         models with no dependencies first).
    107         """
    108         dealt_with = SortedDict()
    109         # Start with items that have no children
    110         models = self.data.keys()
    111         while len(dealt_with) < len(models):
    112             found = False
    113             for model in models:
    114                 if model in dealt_with:
    115                     continue
    116                 children = self.children.setdefault(model, [])
    117                 if len([c for c in children if c not in dealt_with]) == 0:
    118                     dealt_with[model] = None
    119                     found = True
    120             if not found:
    121                 raise CyclicDependency(
    122                     "There is a cyclic dependency of items to be processed.")
    123 
    124         return dealt_with.keys()
    125 
    126     def unordered_keys(self):
    127         """
    128         Fallback for the case where is a cyclic dependency but we don't  care.
    129         """
    130         return self.data.keys()
    131 
    13224class QueryWrapper(object):
    13325    """
    13426    A type that indicates the contents are an SQL fragment and the associate
  • django/core/management/validation.py

     
    2222    from django.db import models, connection
    2323    from django.db.models.loading import get_app_errors
    2424    from django.db.models.fields.related import RelatedObject
     25    from django.db.models.deletion import SET_NULL, SET_DEFAULT
    2526
    2627    e = ModelErrorCollection(outfile)
    2728
     
    8586            # Perform any backend-specific field validation.
    8687            connection.validation.validate_field(e, opts, f)
    8788
     89            # Check if the on_delete behavior is sane
     90            if f.rel and hasattr(f.rel, 'on_delete'):
     91                if f.rel.on_delete == SET_NULL and not f.null:
     92                    e.add(opts, "'%s' specifies on_delete=SET_NULL, but cannot be null." % f.name)
     93                elif f.rel.on_delete == SET_DEFAULT and not f.has_default():
     94                    e.add(opts, "'%s' specifies on_delete=SET_DEFAULT, but has no default value." % f.name)
     95
    8896            # Check to see if the related field will clash with any existing
    8997            # fields, m2m fields, m2m related objects or related objects
    9098            if f.rel:
  • django/contrib/admin/util.py

     
    11from django.db import models
     2from django.db.models.deletion import Collector
    23from django.forms.forms import pretty_name
    34from django.utils import formats
    45from django.utils.html import escape
     
    103104    method uses this function also from a change_list view.
    104105    This will not be used if we can reverse the URL.
    105106    """
     107    # FIXME: This code is broken. The old version depends on Model._collect_sub_objects() and contained a TODO comment.
    106108    collector = NestedObjects()
    107     for obj in objs:
    108         # TODO using a private model API!
    109         obj._collect_sub_objects(collector)
    110 
     109    collector.collect(objs)
    111110    perms_needed = set()
    112 
    113111    to_delete = collector.nested(_format_callback,
    114112                                 user=user,
    115113                                 admin_site=admin_site,
     
    119117    return to_delete, perms_needed
    120118
    121119
    122 class NestedObjects(object):
    123     """
    124     A directed acyclic graph collection that exposes the add() API
    125     expected by Model._collect_sub_objects and can present its data as
    126     a nested list of objects.
    127 
    128     """
    129     def __init__(self):
    130         # Use object keys of the form (model, pk) because actual model
    131         # objects may not be unique
    132 
    133         # maps object key to list of child keys
    134         self.children = SortedDict()
    135 
    136         # maps object key to parent key
    137         self.parents = SortedDict()
    138 
    139         # maps object key to actual object
    140         self.seen = SortedDict()
    141 
    142     def add(self, model, pk, obj,
    143             parent_model=None, parent_obj=None, nullable=False):
    144         """
    145         Add item ``obj`` to the graph. Returns True (and does nothing)
    146         if the item has been seen already.
    147 
    148         The ``parent_obj`` argument must already exist in the graph; if
    149         not, it's ignored (but ``obj`` is still added with no
    150         parent). In any case, Model._collect_sub_objects (for whom
    151         this API exists) will never pass a parent that hasn't already
    152         been added itself.
    153 
    154         These restrictions in combination ensure the graph will remain
    155         acyclic (but can have multiple roots).
    156 
    157         ``model``, ``pk``, and ``parent_model`` arguments are ignored
    158         in favor of the appropriate lookups on ``obj`` and
    159         ``parent_obj``; unlike CollectedObjects, we can't maintain
    160         independence from the knowledge that we're operating on model
    161         instances, and we don't want to allow for inconsistency.
    162 
    163         ``nullable`` arg is ignored: it doesn't affect how the tree of
    164         collected objects should be nested for display.
    165         """
    166         model, pk = type(obj), obj._get_pk_val()
    167 
    168         # auto-created M2M models don't interest us
    169         if model._meta.auto_created:
    170             return True
    171 
    172         key = model, pk
    173 
    174         if key in self.seen:
    175             return True
    176         self.seen.setdefault(key, obj)
    177 
    178         if parent_obj is not None:
    179             parent_model, parent_pk = (type(parent_obj),
    180                                        parent_obj._get_pk_val())
    181             parent_key = (parent_model, parent_pk)
    182             if parent_key in self.seen:
    183                 self.children.setdefault(parent_key, list()).append(key)
    184                 self.parents.setdefault(key, parent_key)
    185 
    186     def _nested(self, key, format_callback=None, **kwargs):
    187         obj = self.seen[key]
    188         if format_callback:
    189             ret = [format_callback(obj, **kwargs)]
    190         else:
    191             ret = [obj]
    192 
    193         children = []
    194         for child in self.children.get(key, ()):
    195             children.extend(self._nested(child, format_callback, **kwargs))
    196         if children:
    197             ret.append(children)
    198 
    199         return ret
    200 
     120class NestedObjects(Collector):
    201121    def nested(self, format_callback=None, **kwargs):
    202         """
    203         Return the graph as a nested list.
     122        return [format_callback(obj, **kwargs) for obj in self]
    204123
    205         Passes **kwargs back to the format_callback as kwargs.
    206124
    207         """
    208         roots = []
    209         for key in self.seen.keys():
    210             if key not in self.parents:
    211                 roots.extend(self._nested(key, format_callback, **kwargs))
    212         return roots
    213 
    214 
    215125def model_format_dict(obj):
    216126    """
    217127    Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
  • tests/modeltests/invalid_models/models.py

     
    207207    tgt = models.ForeignKey(FKTarget, to_field='good')
    208208
    209209
     210class InvalidSetNull(models.Model):
     211    fk = models.ForeignKey('self', on_delete=models.SET_NULL)
     212
     213class InvalidSetDefault(models.Model):
     214    fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT)
     215
     216
    210217model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer.
    211218invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer.
    212219invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer.
     
    311318invalid_models.uniquem2m: ManyToManyFields cannot be unique.  Remove the unique argument on 'unique_people'.
    312319invalid_models.nonuniquefktarget1: Field 'bad' under model 'FKTarget' must have a unique=True constraint.
    313320invalid_models.nonuniquefktarget2: Field 'bad' under model 'FKTarget' must have a unique=True constraint.
     321invalid_models.invalidsetnull: 'fk' specifies on_delete=SET_NULL, but cannot be null.
     322invalid_models.invalidsetdefault: 'fk' specifies on_delete=SET_DEFAULT, but has no default value.
    314323"""
  • tests/modeltests/delete/tests.py

     
    11from django.db.models import sql
    22from django.db.models.loading import cache
    3 from django.db.models.query import CollectedObjects
    4 from django.db.models.query_utils import CyclicDependency
    53from django.test import TestCase
    64
    75from models import A, B, C, D, E, F
     
    2422        self.order_models("a", "b", "c", "d", "e", "f")
    2523        self.clear_rel_obj_caches(A, B, C, D, E, F)
    2624
    27     def test_collected_objects(self):
    28         g = CollectedObjects()
    29         self.assertFalse(g.add("key1", 1, "item1", None))
    30         self.assertEqual(g["key1"], {1: "item1"})
    31 
    32         self.assertFalse(g.add("key2", 1, "item1", "key1"))
    33         self.assertFalse(g.add("key2", 2, "item2", "key1"))
    34 
    35         self.assertEqual(g["key2"], {1: "item1", 2: "item2"})
    36 
    37         self.assertFalse(g.add("key3", 1, "item1", "key1"))
    38         self.assertTrue(g.add("key3", 1, "item1", "key2"))
    39         self.assertEqual(g.ordered_keys(), ["key3", "key2", "key1"])
    40 
    41         self.assertTrue(g.add("key2", 1, "item1", "key3"))
    42         self.assertRaises(CyclicDependency, g.ordered_keys)
    43 
    44     def test_delete(self):
    45         ## Second, test the usage of CollectedObjects by Model.delete()
    46 
    47         # Due to the way that transactions work in the test harness, doing
    48         # m.delete() here can work but fail in a real situation, since it may
    49         # delete all objects, but not in the right order. So we manually check
    50         # that the order of deletion is correct.
    51 
    52         # Also, it is possible that the order is correct 'accidentally', due
    53         # solely to order of imports etc.  To check this, we set the order that
    54         # 'get_models()' will retrieve to a known 'nice' order, and then try
    55         # again with a known 'tricky' order.  Slightly naughty access to
    56         # internals here :-)
    57 
    58         # If implementation changes, then the tests may need to be simplified:
    59         #  - remove the lines that set the .keyOrder and clear the related
    60         #    object caches
    61         #  - remove the second set of tests (with a2, b2 etc)
    62 
    63         a1 = A.objects.create()
    64         b1 = B.objects.create(a=a1)
    65         c1 = C.objects.create(b=b1)
    66         d1 = D.objects.create(c=c1, a=a1)
    67 
    68         o = CollectedObjects()
    69         a1._collect_sub_objects(o)
    70         self.assertEqual(o.keys(), [D, C, B, A])
    71         a1.delete()
    72 
    73         # Same again with a known bad order
    74         self.order_models("d", "c", "b", "a")
    75         self.clear_rel_obj_caches(A, B, C, D)
    76 
    77         a2 = A.objects.create()
    78         b2 = B.objects.create(a=a2)
    79         c2 = C.objects.create(b=b2)
    80         d2 = D.objects.create(c=c2, a=a2)
    81 
    82         o = CollectedObjects()
    83         a2._collect_sub_objects(o)
    84         self.assertEqual(o.keys(), [D, C, B, A])
    85         a2.delete()
    86 
    87     def test_collected_objects_null(self):
    88         g = CollectedObjects()
    89         self.assertFalse(g.add("key1", 1, "item1", None))
    90         self.assertFalse(g.add("key2", 1, "item1", "key1", nullable=True))
    91         self.assertTrue(g.add("key1", 1, "item1", "key2"))
    92         self.assertEqual(g.ordered_keys(), ["key1", "key2"])
    93 
    94     def test_delete_nullable(self):
    95         e1 = E.objects.create()
    96         f1 = F.objects.create(e=e1)
    97         e1.f = f1
    98         e1.save()
    99 
    100         # Since E.f is nullable, we should delete F first (after nulling out
    101         # the E.f field), then E.
    102 
    103         o = CollectedObjects()
    104         e1._collect_sub_objects(o)
    105         self.assertEqual(o.keys(), [F, E])
    106 
    107         # temporarily replace the UpdateQuery class to verify that E.f is
    108         # actually nulled out first
    109 
    110         logged = []
    111         class LoggingUpdateQuery(sql.UpdateQuery):
    112             def clear_related(self, related_field, pk_list, using):
    113                 logged.append(related_field.name)
    114                 return super(LoggingUpdateQuery, self).clear_related(related_field, pk_list, using)
    115         original = sql.UpdateQuery
    116         sql.UpdateQuery = LoggingUpdateQuery
    117 
    118         e1.delete()
    119         self.assertEqual(logged, ["f"])
    120         logged = []
    121 
    122         e2 = E.objects.create()
    123         f2 = F.objects.create(e=e2)
    124         e2.f = f2
    125         e2.save()
    126 
    127         # Same deal as before, though we are starting from the other object.
    128         o = CollectedObjects()
    129         f2._collect_sub_objects(o)
    130         self.assertEqual(o.keys(), [F, E])
    131         f2.delete()
    132         self.assertEqual(logged, ["f"])
    133         logged = []
    134 
    135         sql.UpdateQuery = original
  • tests/modeltests/on_delete/__init__.py

     
     1#
     2 No newline at end of file
  • tests/modeltests/on_delete/models.py

     
     1from django.test import TestCase
     2from django.db import models, IntegrityError
     3
     4class R(models.Model):
     5    is_default = models.BooleanField(default=False)
     6
     7    def __str__(self):
     8        return "%s" % self.pk
     9
     10get_default_r = lambda: R.objects.get_or_create(is_default=True)[0]
     11   
     12class S(models.Model):
     13    r = models.ForeignKey(R)
     14   
     15class T(models.Model):
     16    s = models.ForeignKey(S)
     17
     18class U(models.Model):
     19    t = models.ForeignKey(T)
     20
     21
     22class A(models.Model):
     23    name = models.CharField(max_length=10)   
     24
     25    auto = models.ForeignKey(R, related_name="auto_set")
     26    auto_nullable = models.ForeignKey(R, null=True, related_name='auto_nullable_set')
     27    setnull = models.ForeignKey(R, on_delete=models.SET_NULL, null=True, related_name='setnull_set')
     28    setdefault = models.ForeignKey(R, on_delete=models.SET_DEFAULT, default=get_default_r, related_name='setdefault_set')
     29    setdefault_none = models.ForeignKey(R, on_delete=models.SET_DEFAULT, default=None, null=True, related_name='setnull_nullable_set')
     30    cascade = models.ForeignKey(R, on_delete=models.CASCADE, related_name='cascade_set')
     31    cascade_nullable = models.ForeignKey(R, on_delete=models.CASCADE, null=True, related_name='cascade_nullable_set')
     32    protect = models.ForeignKey(R, on_delete=models.PROTECT, null=True)
     33    donothing = models.ForeignKey(R, on_delete=models.DO_NOTHING, null=True, related_name='donothing_set')
     34   
     35def create_a(name):
     36    a = A(name=name)
     37    for name in ('auto', 'auto_nullable', 'setnull', 'setdefault', 'setdefault_none', 'cascade', 'cascade_nullable', 'protect', 'donothing'):
     38        r = R.objects.create()
     39        setattr(a, name, r)
     40    a.save()
     41    return a
     42   
     43class M(models.Model):
     44    m2m = models.ManyToManyField(R, related_name="m_set")   
     45    m2m_through = models.ManyToManyField(R, through="MR", related_name="m_through_set")
     46    m2m_through_null = models.ManyToManyField(R, through="MRNull", related_name="m_through_null_set")
     47   
     48class MR(models.Model):
     49    m = models.ForeignKey(M)
     50    r = models.ForeignKey(R)
     51
     52class MRNull(models.Model):
     53    m = models.ForeignKey(M)
     54    r = models.ForeignKey(R, null=True, on_delete=models.SET_NULL)
     55
     56class OnDeleteTests(TestCase):
     57    def test_basics(self):
     58        DEFAULT = get_default_r()
     59       
     60        a = create_a('auto')
     61        a.auto.delete()
     62        self.failIf(A.objects.filter(name='auto').exists())
     63       
     64        a = create_a('auto_nullable')
     65        a.auto_nullable.delete()
     66        self.failIf(A.objects.filter(name='auto_nullable').exists())
     67       
     68        a = create_a('setnull')
     69        a.setnull.delete()
     70        a = A.objects.get(pk=a.pk)
     71        self.failUnlessEqual(None, a.setnull)
     72       
     73        a = create_a('setdefault')
     74        a.setdefault.delete()
     75        a = A.objects.get(pk=a.pk)
     76        self.failUnlessEqual(DEFAULT, a.setdefault)
     77       
     78        a = create_a('setdefault_none')
     79        a.setdefault_none.delete()
     80        a = A.objects.get(pk=a.pk)
     81        self.failUnlessEqual(None, a.setdefault_none)
     82       
     83        a = create_a('cascade')
     84        a.cascade.delete()
     85        self.failIf(A.objects.filter(name='cascade').exists())
     86       
     87        a = create_a('cascade_nullable')
     88        a.cascade_nullable.delete()
     89        self.failIf(A.objects.filter(name='cascade_nullable').exists())
     90       
     91        a = create_a('protect')
     92        self.assertRaises(IntegrityError, a.protect.delete)
     93       
     94        # Testing DO_NOTHING is a bit harder: It would raise IntegrityError for a normal model,
     95        # so we connect to pre_delete and set the fk to a known value.
     96        replacement_r = R.objects.create()
     97        def check_do_nothing(sender, **kwargs):
     98            obj = kwargs['instance']
     99            obj.donothing_set.update(donothing=replacement_r)
     100        models.signals.pre_delete.connect(check_do_nothing)
     101        a = create_a('do_nothing')
     102        a.donothing.delete()
     103        a = A.objects.get(pk=a.pk)
     104        self.failUnlessEqual(replacement_r, a.donothing)
     105        models.signals.pre_delete.disconnect(check_do_nothing)       
     106       
     107        A.objects.all().update(protect=None, donothing=None)
     108        R.objects.all().delete()
     109        self.failIf(A.objects.exists())
     110       
     111    def test_m2m(self):
     112        m = M.objects.create()
     113        r = R.objects.create()
     114        MR.objects.create(m=m, r=r)
     115        r.delete()
     116        self.failIf(MR.objects.exists())
     117       
     118        r = R.objects.create()
     119        MR.objects.create(m=m, r=r)
     120        m.delete()
     121        self.failIf(MR.objects.exists())
     122       
     123        m = M.objects.create()
     124        r = R.objects.create()
     125        m.m2m.add(r)
     126        r.delete()
     127        through = M._meta.get_field('m2m').rel.through
     128        self.failIf(through.objects.exists())
     129       
     130        r = R.objects.create()
     131        m.m2m.add(r)
     132        m.delete()
     133        self.failIf(through.objects.exists())
     134       
     135        m = M.objects.create()
     136        r = R.objects.create()
     137        MRNull.objects.create(m=m, r=r)
     138        r.delete()
     139        self.failIf(not MRNull.objects.exists())
     140        self.failIf(m.m2m_through_null.exists())
     141       
     142   
     143    def assert_num_queries(self, num, func, *args, **kwargs):
     144        # FIXME: replace with the new builtin method
     145        from django.conf import settings
     146        from django.db import connection
     147        old_debug = settings.DEBUG
     148        settings.DEBUG = True
     149        query_count = len(connection.queries)
     150        func(*args, **kwargs)
     151        self.failUnlessEqual(num, len(connection.queries) - query_count)
     152        connection.queries = connection.queries[:query_count]
     153        settings.DEBUG = old_debug
     154   
     155    def test_bulk(self):
     156        from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
     157        s = S.objects.create(r=R.objects.create())
     158        for i in xrange(2*GET_ITERATOR_CHUNK_SIZE):
     159            T.objects.create(s=s)
     160        #   1 (select related `T` instances)
     161        # + 1 (select related `U` instances)
     162        # + 2 (delete `T` instances in batches)
     163        # + 1 (delete `s`)
     164        self.assert_num_queries(5, s.delete)
     165        self.failIf(S.objects.exists())
     166       
     167    def test_instance_update(self):
     168        deleted = []
     169        related_setnull_sets = []
     170        def pre_delete(sender, **kwargs):
     171            obj = kwargs['instance']
     172            deleted.append(obj)
     173            if isinstance(obj, R):
     174                related_setnull_sets.append(list(a.pk for a in obj.setnull_set.all()))
     175
     176        models.signals.pre_delete.connect(pre_delete)
     177        a = create_a('update_setnull')
     178        a.setnull.delete()
     179       
     180        a = create_a('update_cascade')
     181        a.cascade.delete()
     182       
     183        for obj in deleted:
     184            self.failUnlessEqual(None, obj.pk)
     185           
     186        for pk_list in related_setnull_sets:
     187            for a in A.objects.filter(id__in=pk_list):
     188                self.failUnlessEqual(None, a.setnull)
     189       
     190        models.signals.pre_delete.disconnect(pre_delete)
     191
     192    def test_deletion_order(self):
     193        pre_delete_order = []
     194        post_delete_order = []
     195
     196        def log_post_delete(sender, **kwargs):
     197            pre_delete_order.append((sender, kwargs['instance'].pk))
     198
     199        def log_pre_delete(sender, **kwargs):
     200            post_delete_order.append((sender, kwargs['instance'].pk))
     201       
     202        models.signals.post_delete.connect(log_post_delete)
     203        models.signals.pre_delete.connect(log_pre_delete)
     204       
     205        r = R.objects.create(pk=1)
     206        s1 = S.objects.create(pk=1, r=r)
     207        s2 = S.objects.create(pk=2, r=r)
     208        t1 = T.objects.create(pk=1, s=s1)
     209        t2 = T.objects.create(pk=2, s=s2)
     210        r.delete()
     211        self.failUnlessEqual(pre_delete_order, [(T, 2), (T, 1), (S, 2), (S, 1), (R, 1)])
     212        self.failUnlessEqual(post_delete_order, [(T, 1), (T, 2), (S, 1), (S, 2), (R, 1)])
     213       
     214        models.signals.post_delete.disconnect(log_post_delete)
     215        models.signals.post_delete.disconnect(log_pre_delete)
     216       
Back to Top