Ticket #7539: 7539.on_delete.r11724.diff

File 7539.on_delete.r11724.diff, 42.0 KB (added by Johannes Dollinger, 14 years ago)
  • 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_through = models.ManyToManyField(R, through="MR", related_name="m_through_set")
     45    m2m = models.ManyToManyField(R, related_name="m_set")
     46   
     47class MR(models.Model):
     48    m = models.ForeignKey(M)
     49    r = models.ForeignKey(R)
     50   
     51
     52class OnDeleteTests(TestCase):
     53    def test_basics(self):
     54        DEFAULT = get_default_r()
     55       
     56        a = create_a('auto')
     57        a.auto.delete()
     58        self.failIf(A.objects.filter(name='auto').exists())
     59       
     60        a = create_a('auto_nullable')
     61        a.auto_nullable.delete()
     62        self.failUnlessEqual([a], list(A.objects.all()))
     63       
     64        a = create_a('setnull')
     65        a.setnull.delete()
     66        a = A.objects.get(pk=a.pk)
     67        self.failUnlessEqual(None, a.setnull)
     68       
     69        a = create_a('setdefault')
     70        a.setdefault.delete()
     71        a = A.objects.get(pk=a.pk)
     72        self.failUnlessEqual(DEFAULT, a.setdefault)
     73       
     74        a = create_a('setdefault_none')
     75        a.setdefault_none.delete()
     76        a = A.objects.get(pk=a.pk)
     77        self.failUnlessEqual(None, a.setdefault_none)
     78       
     79        a = create_a('cascade')
     80        a.cascade.delete()
     81        self.failIf(A.objects.filter(name='cascade').exists())
     82       
     83        a = create_a('cascade_nullable')
     84        a.cascade_nullable.delete()
     85        self.failIf(A.objects.filter(name='cascade_nullable').exists())
     86       
     87        a = create_a('protect')
     88        self.assertRaises(IntegrityError, a.protect.delete)
     89       
     90        # Testing DO_NOTHING is a bit harder: It would raise IntegrityError for a normal model,
     91        # so we connect to pre_delete and set the fk to a known value.
     92        replacement_r = R.objects.create()
     93        def check_do_nothing(sender, **kwargs):
     94            obj = kwargs['instance']
     95            obj.donothing_set.update(donothing=replacement_r)
     96        models.signals.pre_delete.connect(check_do_nothing)
     97        a = create_a('do_nothing')
     98        a.donothing.delete()
     99        a = A.objects.get(pk=a.pk)
     100        self.failUnlessEqual(replacement_r, a.donothing)
     101        models.signals.pre_delete.disconnect(check_do_nothing)       
     102       
     103        # cleanup
     104        A.objects.all().update(protect=None, donothing=None)
     105        R.objects.all().delete()
     106        self.failIf(A.objects.exists())
     107       
     108    def test_m2m(self):
     109        m = M.objects.create()
     110        r = R.objects.create()
     111        MR.objects.create(m=m, r=r)
     112        r.delete()
     113        self.failIf(MR.objects.exists())
     114       
     115        r = R.objects.create()
     116        MR.objects.create(m=m, r=r)
     117        m.delete()
     118        self.failIf(MR.objects.exists())
     119       
     120        m = M.objects.create()
     121        r = R.objects.create()
     122        m.m2m.add(r)
     123        r.delete()
     124        through = M._meta.get_field('m2m').rel.through
     125        self.failIf(through.objects.exists())
     126       
     127        r = R.objects.create()
     128        m.m2m.add(r)
     129        m.delete()
     130        self.failIf(through.objects.exists())
     131   
     132    def assert_num_queries(self, num, func, *args, **kwargs):
     133        from django.conf import settings
     134        from django.db import connection
     135        old_debug = settings.DEBUG
     136        settings.DEBUG = True
     137        query_count = len(connection.queries)
     138        func(*args, **kwargs)
     139        self.failUnlessEqual(num, len(connection.queries) - query_count)
     140        connection.queries = connection.queries[:query_count]
     141        settings.DEBUG = old_debug
     142   
     143    def test_bulk(self):
     144        from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
     145        s = S.objects.create(r=R.objects.create())
     146        for i in xrange(2*GET_ITERATOR_CHUNK_SIZE):
     147            T.objects.create(s=s)
     148        #   1 (select related `T` instances)
     149        # + 1 (select related `U` instances)
     150        # + 2 (delete `T` instances in batches)
     151        # + 1 (delete `s`)
     152        self.assert_num_queries(5, s.delete)
     153        self.failIf(S.objects.exists())
     154       
     155    def test_cache_update(self):
     156        deleted = []
     157        related_setnull_sets = []
     158        def pre_delete(sender, **kwargs):
     159            obj = kwargs['instance']
     160            deleted.append(obj)
     161            if isinstance(obj, R):
     162                related_setnull_sets.append(list(a.pk for a in obj.setnull_set.all()))
     163
     164        models.signals.pre_delete.connect(pre_delete)
     165        a = create_a('cache_update_setnull')
     166        a.setnull.delete()
     167       
     168        a = create_a('cache_update_setnull')
     169        a.cascade.delete()
     170       
     171        for obj in deleted:
     172            self.failUnlessEqual(None, obj.pk)
     173           
     174        for pk_list in related_setnull_sets:
     175            for a in A.objects.filter(id__in=pk_list):
     176                self.failUnlessEqual(None, a.setnull)
     177       
     178        models.signals.pre_delete.disconnect(pre_delete)
     179
     180    def test_deletion_order(self):
     181        pre_delete_order = []
     182        post_delete_order = []
     183
     184        def log_post_delete(sender, **kwargs):
     185            pre_delete_order.append((sender, kwargs['instance'].pk))
     186
     187        def log_pre_delete(sender, **kwargs):
     188            post_delete_order.append((sender, kwargs['instance'].pk))
     189       
     190        models.signals.post_delete.connect(log_post_delete)
     191        models.signals.pre_delete.connect(log_pre_delete)
     192       
     193        r = R.objects.create(pk=1)
     194        s1 = S.objects.create(pk=1, r=r)
     195        s2 = S.objects.create(pk=2, r=r)
     196        t1 = T.objects.create(pk=1, s=s1)
     197        t2 = T.objects.create(pk=2, s=s2)
     198        r.delete()
     199        self.failUnlessEqual(pre_delete_order, [(T, 2), (T, 1), (S, 2), (S, 1), (R, 1)])
     200        self.failUnlessEqual(post_delete_order, [(T, 1), (T, 2), (S, 1), (S, 2), (R, 1)])
     201       
     202        models.signals.post_delete.disconnect(log_post_delete)
     203        models.signals.post_delete.disconnect(log_pre_delete)
     204       
  • tests/modeltests/delete/models.py

     
    4444__test__ = {'API_TESTS': """
    4545### Tests for models A,B,C,D ###
    4646
    47 ## First, test the CollectedObjects data structure directly
    48 
    49 >>> from django.db.models.query import CollectedObjects
    50 
    51 >>> g = CollectedObjects()
    52 >>> g.add("key1", 1, "item1", None)
    53 False
    54 >>> g["key1"]
    55 {1: 'item1'}
    56 >>> g.add("key2", 1, "item1", "key1")
    57 False
    58 >>> g.add("key2", 2, "item2", "key1")
    59 False
    60 >>> g["key2"]
    61 {1: 'item1', 2: 'item2'}
    62 >>> g.add("key3", 1, "item1", "key1")
    63 False
    64 >>> g.add("key3", 1, "item1", "key2")
    65 True
    66 >>> g.ordered_keys()
    67 ['key3', 'key2', 'key1']
    68 
    69 >>> g.add("key2", 1, "item1", "key3")
    70 True
    71 >>> g.ordered_keys()
    72 Traceback (most recent call last):
    73     ...
    74 CyclicDependency: There is a cyclic dependency of items to be processed.
    75 
    76 
    77 ## Second, test the usage of CollectedObjects by Model.delete()
    78 
    7947# Due to the way that transactions work in the test harness,
    8048# doing m.delete() here can work but fail in a real situation,
    8149# since it may delete all objects, but not in the right order.
     
    11179>>> c1.save()
    11280>>> d1 = D(c=c1, a=a1)
    11381>>> d1.save()
    114 
    115 >>> o = CollectedObjects()
    116 >>> a1._collect_sub_objects(o)
    117 >>> o.keys()
    118 [<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
    11982>>> a1.delete()
    12083
    12184# Same again with a known bad order
     
    13093>>> c2.save()
    13194>>> d2 = D(c=c2, a=a2)
    13295>>> d2.save()
    133 
    134 >>> o = CollectedObjects()
    135 >>> a2._collect_sub_objects(o)
    136 >>> o.keys()
    137 [<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
    13896>>> a2.delete()
    13997
    14098### Tests for models E,F - nullable related fields ###
    14199
    142 ## First, test the CollectedObjects data structure directly
    143 
    144 >>> g = CollectedObjects()
    145 >>> g.add("key1", 1, "item1", None)
    146 False
    147 >>> g.add("key2", 1, "item1", "key1", nullable=True)
    148 False
    149 >>> g.add("key1", 1, "item1", "key2")
    150 True
    151 >>> g.ordered_keys()
    152 ['key1', 'key2']
    153 
    154 ## Second, test the usage of CollectedObjects by Model.delete()
    155 
    156100>>> e1 = E()
    157101>>> e1.save()
    158102>>> f1 = F(e=e1)
    159103>>> f1.save()
    160104>>> e1.f = f1
    161105>>> e1.save()
    162 
    163 # Since E.f is nullable, we should delete F first (after nulling out
    164 # the E.f field), then E.
    165 
    166 >>> o = CollectedObjects()
    167 >>> e1._collect_sub_objects(o)
    168 >>> o.keys()
    169 [<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]
    170 
    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
    179106>>> e1.delete()
    180 CLEARING FIELD f
    181107
    182108>>> e2 = E()
    183109>>> e2.save()
     
    185111>>> f2.save()
    186112>>> e2.f = f2
    187113>>> e2.save()
    188 
    189 # Same deal as before, though we are starting from the other object.
    190 
    191 >>> o = CollectedObjects()
    192 >>> f2._collect_sub_objects(o)
    193 >>> o.keys()
    194 [<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]
    195 
    196114>>> f2.delete()
    197 CLEARING FIELD f
    198 
    199 # Put this back to normal
    200 >>> django.db.models.sql.UpdateQuery = original_class
    201115"""
    202116}
  • django/db/models/sql/subqueries.py

     
    3434        self.where = where
    3535        self.execute_sql(None)
    3636
    37     def delete_batch_related(self, pk_list):
     37    def delete_generic_relation_hack(self, pk_list):
    3838        """
    39         Set up and execute delete queries for all the objects related to the
    40         primary key values in pk_list. To delete the objects themselves, use
    41         the delete_batch() method.
    42 
    43         More than one physical query may be executed if there are a
    44         lot of values in pk_list.
     39        Delete objects related to self.model through a GenericRelation.
     40        This should be handled by a custom `on_delete` handler in django.contrib.contentttypes.
    4541        """
    4642        from django.contrib.contenttypes import generic
    4743        cls = self.model
    48         for related in cls._meta.get_all_related_many_to_many_objects():
    49             if not isinstance(related.field, generic.GenericRelation):
    50                 for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
    51                     where = self.where_class()
    52                     where.add((Constraint(None,
    53                             related.field.m2m_reverse_name(), related.field),
    54                             'in',
    55                             pk_list[offset : offset+GET_ITERATOR_CHUNK_SIZE]),
    56                             AND)
    57                     self.do_query(related.field.m2m_db_table(), where)
    58 
    5944        for f in cls._meta.many_to_many:
    60             w1 = self.where_class()
    61             if isinstance(f, generic.GenericRelation):
    62                 from django.contrib.contenttypes.models import ContentType
    63                 field = f.rel.to._meta.get_field(f.content_type_field_name)
    64                 w1.add((Constraint(None, field.column, field), 'exact',
    65                         ContentType.objects.get_for_model(cls).id), AND)
     45            if not isinstance(f, generic.GenericRelation):
     46                continue
     47            w1 = self.where_class()           
     48            from django.contrib.contenttypes.models import ContentType
     49            field = f.rel.to._meta.get_field(f.content_type_field_name)
     50            w1.add((Constraint(None, field.column, field), 'exact',
     51                    ContentType.objects.get_for_model(cls).id), AND)
    6652            for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
    6753                where = self.where_class()
    6854                where.add((Constraint(None, f.m2m_column_name(), f), 'in',
    6955                        pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]),
    7056                        AND)
    71                 if w1:
    72                     where.add(w1, AND)
     57                where.add(w1, AND)
    7358                self.do_query(f.m2m_db_table(), where)
    7459
    75     def delete_batch(self, pk_list):
     60    def delete_batch(self, pk_list, field=None):
    7661        """
    77         Set up and execute delete queries for all the objects in pk_list. This
    78         should be called after delete_batch_related(), if necessary.
     62        Set up and execute delete queries for all the objects in pk_list.
    7963
    8064        More than one physical query may be executed if there are a
    8165        lot of values in pk_list.
    8266        """
     67        if not field:
     68            field = self.model._meta.pk
    8369        for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
    8470            where = self.where_class()
    85             field = self.model._meta.pk
    8671            where.add((Constraint(None, field.column, field), 'in',
    8772                    pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND)
    8873            self.do_query(self.model._meta.db_table, where)
     
    201186        for alias in self.tables[1:]:
    202187            self.alias_refcount[alias] = 0
    203188
    204     def clear_related(self, related_field, pk_list):
    205         """
    206         Set up and execute an update query that clears related entries for the
    207         keys in pk_list.
    208 
    209         This is used by the QuerySet.delete_objects() method.
    210         """
     189    def update_batch(self, pk_list, values):
     190        pk_field = self.model._meta.pk
     191        self.add_update_values(values)
    211192        for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
    212             self.where = self.where_class()
    213             f = self.model._meta.pk
    214             self.where.add((Constraint(None, f.column, f), 'in',
    215                     pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]),
    216                     AND)
    217             self.values = [(related_field.column, None, '%s')]
     193            self.where = self.where_class()           
     194            self.where.add((Constraint(None, pk_field.column, pk_field), 'in', pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND)
    218195            self.execute_sql(None)
    219196
    220197    def add_update_values(self, values):
  • django/db/models/base.py

     
    77from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
    88from django.db.models.fields import AutoField, FieldDoesNotExist
    99from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
    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 CollectedObjects
    1213from django.db.models.options import Options
    1314from django.db import connection, transaction, DatabaseError
    1415from django.db.models import signals
     
    513514
    514515    save_base.alters_data = True
    515516
    516     def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
    517         """
    518         Recursively populates seen_objs with all objects related to this
    519         object.
    520 
    521         When done, seen_objs.items() will be in the format:
    522             [(model_class, {pk_val: obj, pk_val: obj, ...}),
    523              (model_class, {pk_val: obj, pk_val: obj, ...}), ...]
    524         """
    525         pk_val = self._get_pk_val()
    526         if seen_objs.add(self.__class__, pk_val, self, parent, nullable):
    527             return
    528 
    529         for related in self._meta.get_all_related_objects():
    530             rel_opts_name = related.get_accessor_name()
    531             if isinstance(related.field.rel, OneToOneRel):
    532                 try:
    533                     sub_obj = getattr(self, rel_opts_name)
    534                 except ObjectDoesNotExist:
    535                     pass
    536                 else:
    537                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
    538             else:
    539                 # To make sure we can access all elements, we can't use the
    540                 # normal manager on the related object. So we work directly
    541                 # with the descriptor object.
    542                 for cls in self.__class__.mro():
    543                     if rel_opts_name in cls.__dict__:
    544                         rel_descriptor = cls.__dict__[rel_opts_name]
    545                         break
    546                 else:
    547                     # in the case of a hidden fkey just skip it, it'll get
    548                     # processed as an m2m
    549                     if not related.field.rel.is_hidden():
    550                         raise AssertionError("Should never get here.")
    551                     else:
    552                         continue
    553                 delete_qs = rel_descriptor.delete_manager(self).all()
    554                 for sub_obj in delete_qs:
    555                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
    556 
    557         # Handle any ancestors (for the model-inheritance case). We do this by
    558         # traversing to the most remote parent classes -- those with no parents
    559         # themselves -- and then adding those instances to the collection. That
    560         # will include all the child instances down to "self".
    561         parent_stack = [p for p in self._meta.parents.values() if p is not None]
    562         while parent_stack:
    563             link = parent_stack.pop()
    564             parent_obj = getattr(self, link.name)
    565             if parent_obj._meta.parents:
    566                 parent_stack.extend(parent_obj._meta.parents.values())
    567                 continue
    568             # At this point, parent_obj is base class (no ancestor models). So
    569             # delete it and all its descendents.
    570             parent_obj._collect_sub_objects(seen_objs)
    571 
    572517    def delete(self):
    573518        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)
    574519
    575520        # Find all the objects than need to be deleted.
    576521        seen_objs = CollectedObjects()
    577         self._collect_sub_objects(seen_objs)
     522        seen_objs.collect([self])
    578523
    579524        # Actually delete the objects.
    580         delete_objects(seen_objs)
     525        seen_objs.delete()
    581526
    582527    delete.alters_data = True
    583528
  • django/db/models/options.py

     
    376376                    cache[obj] = parent
    377377                else:
    378378                    cache[obj] = model
    379         for klass in get_models():
     379        for klass in get_models(include_auto_created=True):
    380380            for f in klass._meta.local_fields:
    381381                if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
    382382                    cache[RelatedObject(f.rel.to, klass, f)] = None
  • django/db/models/fields/related.py

     
    1 from django.db import connection, transaction
     1from django.db import connection, transaction, IntegrityError
    22from django.db.backends import util
    33from django.db.models import signals, get_model
    44from django.db.models.fields import AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist
     
    1818
    1919RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
    2020
     21def CASCADE(seen_objs, parent_model, related, sub_objs):
     22    seen_objs.collect(sub_objs, parent_model)
     23
     24def PROTECT(seen_objs, parent_model, related, sub_objs):
     25    msg = "Cannot delete some instances of model '%s' because they are referenced through a protected foreign key: '%s.%s'" % (
     26        parent_model.__name__, related.model.__name__, related.field.name
     27    )
     28    raise IntegrityError(msg)
     29
     30def SET(value):
     31    def set_on_delete(seen_objs, parent_model, related, sub_objs):
     32        seen_objs.add_field_update(related.field, value, sub_objs)
     33    return set_on_delete
     34
     35def SET_NULL(seen_objs, parent_model, related, sub_objs):
     36    seen_objs.add_field_update(related.field, None, sub_objs)
     37
     38def SET_DEFAULT(seen_objs, parent_model, related, sub_objs):
     39    seen_objs.add_field_update(related.field, related.field.get_default(), sub_objs)
     40
     41def DO_NOTHING(seen_objs, parent_model, related, sub_objs):
     42    pass
     43
     44
    2145pending_lookups = {}
    2246
    2347def add_lazy_relation(cls, field, relation, operation):
     
    625649
    626650class ManyToOneRel(object):
    627651    def __init__(self, to, field_name, related_name=None,
    628             limit_choices_to=None, lookup_overrides=None, parent_link=False):
     652            limit_choices_to=None, lookup_overrides=None, parent_link=False,
     653            on_delete=None):
    629654        try:
    630655            to._meta
    631656        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
     
    638663        self.lookup_overrides = lookup_overrides or {}
    639664        self.multiple = True
    640665        self.parent_link = parent_link
     666        self.on_delete = on_delete
    641667
    642668    def is_hidden(self):
    643669        "Should the related object be hidden?"
     
    656682
    657683class OneToOneRel(ManyToOneRel):
    658684    def __init__(self, to, field_name, related_name=None,
    659             limit_choices_to=None, lookup_overrides=None, parent_link=False):
     685            limit_choices_to=None, lookup_overrides=None, parent_link=False,
     686            on_delete=None):
    660687        super(OneToOneRel, self).__init__(to, field_name,
    661688                related_name=related_name, limit_choices_to=limit_choices_to,
    662                 lookup_overrides=lookup_overrides, parent_link=parent_link)
     689                lookup_overrides=lookup_overrides, parent_link=parent_link,
     690                on_delete=on_delete)
    663691        self.multiple = False
    664692
    665693class ManyToManyRel(object):
     
    696724        else:
    697725            assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
    698726        kwargs['verbose_name'] = kwargs.get('verbose_name', None)
     727       
     728        on_delete = kwargs.pop('on_delete', None)
     729        if on_delete is None:
     730            if kwargs.get('null', False):
     731                on_delete = SET_NULL
     732            else:
     733                on_delete = CASCADE
    699734
    700735        kwargs['rel'] = rel_class(to, to_field,
    701736            related_name=kwargs.pop('related_name', None),
    702737            limit_choices_to=kwargs.pop('limit_choices_to', None),
    703738            lookup_overrides=kwargs.pop('lookup_overrides', None),
    704             parent_link=kwargs.pop('parent_link', False))
     739            parent_link=kwargs.pop('parent_link', False),
     740            on_delete=on_delete)
    705741        Field.__init__(self, **kwargs)
    706742
    707743        self.db_index = True
     
    746782            target = self.rel.to._meta.db_table
    747783        cls._meta.duplicate_targets[self.column] = (target, "o2m")
    748784
     785        on_delete = self.rel.on_delete
     786        if on_delete == SET_NULL and not self.null:
     787            specification = "'on_delete=SET_NULL'"
     788            raise ValueError("%s specified for %s '%s.%s', but the field is not nullable." % (specification, type(self).__name__, cls.__name__, name))
     789        if on_delete == SET_DEFAULT and not self.has_default():
     790            specification = "'on_delete=SET_DEFAULT'"
     791            raise ValueError("%s specified for %s '%s.%s', but the field has no default value." % (specification, type(self).__name__, cls.__name__, name))
     792
    749793    def contribute_to_related_class(self, cls, related):
    750794        # Internal FK's - i.e., those with a related name ending with '+' -
    751795        # don't get a related descriptor.
  • 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, 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 connection, transaction
     4from django.db.models import signals, sql
     5from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
     6
     7def ensure_transaction(func):
     8    @wraps(func)
     9    def decorated(*args, **kwargs):
     10        if not transaction.is_managed():
     11            transaction.enter_transaction_management()
     12            forced_managed = True
     13        else:
     14            forced_managed = False
     15        try:                   
     16            func(*args, **kwargs)
     17            if forced_managed:
     18                transaction.commit()
     19            else:
     20                transaction.commit_unless_managed()
     21        finally:
     22            if forced_managed:
     23                transaction.leave_transaction_management()
     24    return decorated
     25
     26class CollectedObjects(object):
     27    def __init__(self, previously_seen=None):
     28        # {model: {(field, value): set([instances])}}
     29        self.field_updates = {}
     30        self.data = {}
     31        self.batches = {}
     32        self.children = {}
     33        if previously_seen:
     34            self.blocked = set(previously_seen)
     35            self.blocked.update(previously_seen.blocked)
     36        else:
     37            self.blocked = set()
     38
     39    def add(self, objs, parent_model=None):
     40        if not objs:
     41            return []
     42        new_objs = []
     43        model = objs[0].__class__
     44        instances = self.data.setdefault(model, [])
     45        for obj in objs:
     46            if obj not in instances and obj not in self.blocked:
     47                new_objs.append(obj)
     48        instances.extend(new_objs)
     49        # Nullable relationships can be ignored -- they are nulled out before
     50        # deleting, and therefore do not affect the order in which objects
     51        # have to be deleted.
     52        if new_objs and parent_model is not None:
     53            self.children.setdefault(parent_model, set()).add(model)
     54        return new_objs
     55       
     56    def add_batch(self, related, objs):
     57        batches = self.batches.setdefault(related.model, dict())
     58        batches[related.field] = objs
     59       
     60    def add_field_update(self, field, value, objs):
     61        if not objs:
     62            return
     63        model = objs[0].__class__
     64        updates = self.field_updates.setdefault(model, dict())
     65        updates.setdefault((field, value), set()).update(objs)
     66       
     67    def collect(self, instances, parent=None, collect_related=True):
     68        new_objs = self.add(instances, parent)
     69        if not new_objs:
     70            return
     71        model = new_objs[0].__class__
     72       
     73        # Recusively collect parent models, but not their related objects.
     74        for parent, ptr in model._meta.parents.items():
     75            if ptr:
     76                parent_objs = [getattr(obj, ptr.name) for obj in new_objs]
     77                self.collect(parent_objs, model, collect_related=False)
     78
     79        if collect_related:
     80            for related in model._meta.get_all_related_objects():
     81                if related.model._meta.auto_created:
     82                    self.add_batch(related, new_objs)
     83                else:
     84                    sub_objs = related.model._base_manager.filter(**{"%s__in" % related.field.name: new_objs})
     85                    if sub_objs:
     86                        related.field.rel.on_delete(self, model, related, sub_objs)
     87
     88    def instances_with_model(self):
     89        for model, instances in self.data.iteritems():
     90            for obj in instances:
     91                yield model, obj
     92               
     93    def __iter__(self):
     94        for model, obj in self.instances_with_model():
     95            yield obj
     96
     97    def __nonzero__(self):
     98        return bool(self.data)
     99
     100    def sort(self):
     101        sorted_models = []
     102        models = self.data.keys()
     103        while len(sorted_models) < len(models):
     104            found = False
     105            for model in models:
     106                if model in sorted_models:
     107                    continue
     108                children = self.children.get(model)
     109                if not children or not children.difference(sorted_models):
     110                    sorted_models.append(model)
     111                    found = True
     112            if not found:
     113                return
     114        self.data = SortedDict([(model, self.data[model]) for model in sorted_models])
     115   
     116    @ensure_transaction
     117    def delete(self):
     118        # sort instance collections
     119        for instances in self.data.itervalues():
     120            instances.sort(key=lambda obj: obj.pk)
     121
     122        # if possible, bring models in an order suitable for databases that don't support transactions
     123        # or cannot defer contraint checks until the end of a transaction.
     124        self.sort()
     125       
     126        # send pre_delete signals
     127        for model, obj in self.instances_with_model():
     128            if not model._meta.auto_created:
     129                signals.pre_delete.send(sender=model, instance=obj)
     130
     131        # update fields
     132        for model, instances_for_fieldvalues in self.field_updates.iteritems():
     133            query = sql.UpdateQuery(model, connection)
     134            for (field, value), instances in instances_for_fieldvalues.iteritems():
     135                query.update_batch([obj.pk for obj in instances], {field.name: value})
     136
     137        # reverse instance collections
     138        for instances in self.data.itervalues():
     139            instances.reverse()
     140
     141        # delete batches
     142        for model, batches in self.batches.iteritems():
     143            query = sql.DeleteQuery(model, connection)
     144            for field, instances in batches.iteritems():
     145                query.delete_batch([obj.pk for obj in instances], field)
     146
     147        # delete instances
     148        for model, instances in self.data.iteritems():
     149            query = sql.DeleteQuery(model, connection)
     150            pk_list = [obj.pk for obj in instances]
     151            query.delete_generic_relation_hack(pk_list)           
     152            query.delete_batch(pk_list)
     153       
     154        # send post_delete signals
     155        for model, obj in self.instances_with_model():
     156            if not model._meta.auto_created:
     157                signals.post_delete.send(sender=model, instance=obj)
     158       
     159        # update collected instances
     160        for model, instances_for_fieldvalues in self.field_updates.iteritems():
     161            for (field, value), instances in instances_for_fieldvalues.iteritems():
     162                for obj in instances:
     163                    setattr(obj, field.attname, value)
     164        for model, instances in self.data.iteritems():
     165            for instance in instances:
     166                setattr(instance, model._meta.pk.attname, None)
  • django/db/models/query.py

     
    66from django.db import connection, transaction, IntegrityError
    77from django.db.models.aggregates import Aggregate
    88from django.db.models.fields import DateField
    9 from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory
     9from django.db.models.query_utils import Q, select_related_descend, deferred_class_factory
     10from django.db.models.deletion import CollectedObjects
    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)
    387             for object in del_query[:CHUNK_SIZE]:
    388                 object._collect_sub_objects(seen_objs)
     388            seen_objs.collect(del_query[:CHUNK_SIZE])
    389389
    390390            if not seen_objs:
    391391                break
    392             delete_objects(seen_objs)
     392            seen_objs.delete()
    393393
    394394        # Clear the result cache, in case this QuerySet gets reused.
    395395        self._result_cache = None
     
    10001000                setattr(obj, f.get_cache_name(), rel_obj)
    10011001    return obj, index_end
    10021002
    1003 def delete_objects(seen_objs):
    1004     """
    1005     Iterate through a list of seen classes, and remove any instances that are
    1006     referred to.
    1007     """
    1008     if not transaction.is_managed():
    1009         transaction.enter_transaction_management()
    1010         forced_managed = True
    1011     else:
    1012         forced_managed = False
    1013     try:
    1014         ordered_classes = seen_objs.keys()
    1015     except CyclicDependency:
    1016         # If there is a cyclic dependency, we cannot in general delete the
    1017         # objects.  However, if an appropriate transaction is set up, or if the
    1018         # database is lax enough, it will succeed. So for now, we go ahead and
    1019         # try anyway.
    1020         ordered_classes = seen_objs.unordered_keys()
    10211003
    1022     obj_pairs = {}
    1023     try:
    1024         for cls in ordered_classes:
    1025             items = seen_objs[cls].items()
    1026             items.sort()
    1027             obj_pairs[cls] = items
    1028 
    1029             # Pre-notify all instances to be deleted.
    1030             for pk_val, instance in items:
    1031                 if not cls._meta.auto_created:
    1032                     signals.pre_delete.send(sender=cls, instance=instance)
    1033 
    1034             pk_list = [pk for pk,instance in items]
    1035             del_query = sql.DeleteQuery(cls, connection)
    1036             del_query.delete_batch_related(pk_list)
    1037 
    1038             update_query = sql.UpdateQuery(cls, connection)
    1039             for field, model in cls._meta.get_fields_with_model():
    1040                 if (field.rel and field.null and field.rel.to in seen_objs and
    1041                         filter(lambda f: f.column == field.rel.get_related_field().column,
    1042                         field.rel.to._meta.fields)):
    1043                     if model:
    1044                         sql.UpdateQuery(model, connection).clear_related(field,
    1045                                 pk_list)
    1046                     else:
    1047                         update_query.clear_related(field, pk_list)
    1048 
    1049         # Now delete the actual data.
    1050         for cls in ordered_classes:
    1051             items = obj_pairs[cls]
    1052             items.reverse()
    1053 
    1054             pk_list = [pk for pk,instance in items]
    1055             del_query = sql.DeleteQuery(cls, connection)
    1056             del_query.delete_batch(pk_list)
    1057 
    1058             # Last cleanup; set NULLs where there once was a reference to the
    1059             # object, NULL the primary key of the found objects, and perform
    1060             # post-notification.
    1061             for pk_val, instance in items:
    1062                 for field in cls._meta.fields:
    1063                     if field.rel and field.null and field.rel.to in seen_objs:
    1064                         setattr(instance, field.attname, None)
    1065 
    1066                 if not cls._meta.auto_created:
    1067                     signals.post_delete.send(sender=cls, instance=instance)
    1068                 setattr(instance, cls._meta.pk.attname, None)
    1069 
    1070         if forced_managed:
    1071             transaction.commit()
    1072         else:
    1073             transaction.commit_unless_managed()
    1074     finally:
    1075         if forced_managed:
    1076             transaction.leave_transaction_management()
    1077 
    1078 
    10791004def insert_query(model, values, return_id=False, raw_values=False):
    10801005    """
    10811006    Inserts a new record for the given model. This provides an interface to
  • django/db/models/query_utils.py

     
    1010from copy import deepcopy
    1111
    1212from django.utils import tree
    13 from django.utils.datastructures import SortedDict
    1413
    1514try:
    1615    sorted
    1716except NameError:
    1817    from django.utils.itercompat import sorted  # For Python 2.3.
    1918
    20 
    21 class CyclicDependency(Exception):
    22     """
    23     An error when dealing with a collection of objects that have a cyclic
    24     dependency, i.e. when deleting multiple objects.
    25     """
    26     pass
    27 
    28 class CollectedObjects(object):
    29     """
    30     A container that stores keys and lists of values along with remembering the
    31     parent objects for all the keys.
    32 
    33     This is used for the database object deletion routines so that we can
    34     calculate the 'leaf' objects which should be deleted first.
    35 
    36     previously_seen is an optional argument. It must be a CollectedObjects
    37     instance itself; any previously_seen collected object will be blocked from
    38     being added to this instance.
    39     """
    40 
    41     def __init__(self, previously_seen=None):
    42         self.data = {}
    43         self.children = {}
    44         if previously_seen:
    45             self.blocked = previously_seen.blocked
    46             for cls, seen in previously_seen.data.items():
    47                 self.blocked.setdefault(cls, SortedDict()).update(seen)
    48         else:
    49             self.blocked = {}
    50 
    51     def add(self, model, pk, obj, parent_model, nullable=False):
    52         """
    53         Adds an item to the container.
    54 
    55         Arguments:
    56         * model - the class of the object being added.
    57         * pk - the primary key.
    58         * obj - the object itself.
    59         * parent_model - the model of the parent object that this object was
    60           reached through.
    61         * nullable - should be True if this relation is nullable.
    62 
    63         Returns True if the item already existed in the structure and
    64         False otherwise.
    65         """
    66         if pk in self.blocked.get(model, {}):
    67             return True
    68 
    69         d = self.data.setdefault(model, SortedDict())
    70         retval = pk in d
    71         d[pk] = obj
    72         # Nullable relationships can be ignored -- they are nulled out before
    73         # deleting, and therefore do not affect the order in which objects
    74         # have to be deleted.
    75         if parent_model is not None and not nullable:
    76             self.children.setdefault(parent_model, []).append(model)
    77         return retval
    78 
    79     def __contains__(self, key):
    80         return self.data.__contains__(key)
    81 
    82     def __getitem__(self, key):
    83         return self.data[key]
    84 
    85     def __nonzero__(self):
    86         return bool(self.data)
    87 
    88     def iteritems(self):
    89         for k in self.ordered_keys():
    90             yield k, self[k]
    91 
    92     def items(self):
    93         return list(self.iteritems())
    94 
    95     def keys(self):
    96         return self.ordered_keys()
    97 
    98     def ordered_keys(self):
    99         """
    100         Returns the models in the order that they should be dealt with (i.e.
    101         models with no dependencies first).
    102         """
    103         dealt_with = SortedDict()
    104         # Start with items that have no children
    105         models = self.data.keys()
    106         while len(dealt_with) < len(models):
    107             found = False
    108             for model in models:
    109                 if model in dealt_with:
    110                     continue
    111                 children = self.children.setdefault(model, [])
    112                 if len([c for c in children if c not in dealt_with]) == 0:
    113                     dealt_with[model] = None
    114                     found = True
    115             if not found:
    116                 raise CyclicDependency(
    117                     "There is a cyclic dependency of items to be processed.")
    118 
    119         return dealt_with.keys()
    120 
    121     def unordered_keys(self):
    122         """
    123         Fallback for the case where is a cyclic dependency but we don't  care.
    124         """
    125         return self.data.keys()
    126 
    12719class QueryWrapper(object):
    12820    """
    12921    A type that indicates the contents are an SQL fragment and the associate
Back to Top