Ticket #7539: on_delete_on_update-r11620.diff
File on_delete_on_update-r11620.diff, 21.0 KB (added by , 15 years ago) |
---|
-
django/db/models/base.py
MJG-MBP:django_on_delete_patch mjg$ svn diff
13 13 from django.db.models.fields import AutoField, FieldDoesNotExist 14 14 from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField 15 15 from django.db.models.query import delete_objects, Q 16 from django.db.models.query_utils import Collected Objects, DeferredAttribute16 from django.db.models.query_utils import CollectedFields, CollectedObjects, DeferredAttribute 17 17 from django.db.models.options import Options 18 from django.db import connection, transaction, DatabaseError 18 from django.db import connection, transaction, DatabaseError, IntegrityError 19 19 from django.db.models import signals 20 from django.db.models.fields.related import CASCADE, PROTECT, SET_NULL, SET_DEFAULT 20 21 from django.db.models.loading import register_models, get_model 21 22 from django.utils.functional import curry 22 23 from django.utils.encoding import smart_str, force_unicode, smart_unicode … … 507 508 508 509 save_base.alters_data = True 509 510 510 def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):511 def _collect_sub_objects(self, seen_objs, fields_to_set, parent=None, nullable=False): 511 512 """ 512 513 Recursively populates seen_objs with all objects related to this 513 514 object. … … 519 520 pk_val = self._get_pk_val() 520 521 if seen_objs.add(self.__class__, pk_val, self, parent, nullable): 521 522 return 523 524 def _handle_sub_obj(related, sub_obj): 525 on_delete = related.field.rel.on_delete 526 if on_delete is None: 527 #If no explicit on_delete option is specified, use the old 528 #django behavior as the default: SET_NULL if the foreign 529 #key is nullable, otherwise CASCADE. 530 if related.field.null: 531 on_delete = SET_NULL 532 else: 533 on_delete = CASCADE 534 535 if on_delete == CASCADE: 536 sub_obj._collect_sub_objects(seen_objs, fields_to_set, self.__class__) 537 elif on_delete == PROTECT: 538 msg = '[Django] Cannot delete a parent object: a foreign key constraint fails (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % ( 539 sub_obj.__class__, 540 sub_obj._get_pk_val(), 541 self.__class__, 542 pk_val, 543 ) 544 raise IntegrityError(msg) 545 elif on_delete == SET_NULL: 546 if not related.field.null: 547 msg = '[Django] Cannot delete a parent object: foreign key constraint on_delete=SET_NULL is specified for a non-nullable foreign key (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % ( 548 sub_obj.__class__, 549 sub_obj._get_pk_val(), 550 self.__class__, 551 pk_val, 552 ) 553 raise IntegrityError(msg) 554 fields_to_set.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name, None) 555 elif on_delete == SET_DEFAULT: 556 if not related.field.has_default(): 557 msg = '[Django] Cannot delete a parent object: foreign key constraint on_delete=SET_DEFAULT is specified for a foreign key with no default value (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % ( 558 sub_obj.__class__, 559 sub_obj._get_pk_val(), 560 self.__class__, 561 pk_val, 562 ) 563 raise IntegrityError(msg) 564 fields_to_set.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name, related.field.get_default()) 565 else: 566 raise AttributeError('Unexpected value for on_delete') 522 567 523 568 for related in self._meta.get_all_related_objects(): 524 569 rel_opts_name = related.get_accessor_name() 525 570 if isinstance(related.field.rel, OneToOneRel): 526 571 try: 572 # delattr(self, rel_opts_name) #Delete first to clear any stale cache 573 #TODO: the above line is a bit of a hack 574 #It's one way (not a very good one) to work around stale cache data causing 575 #spurious RESTRICT errors, etc; it would be better to prevent the cache from 576 #becoming stale in the first place. 527 577 sub_obj = getattr(self, rel_opts_name) 528 578 except ObjectDoesNotExist: 529 579 pass 530 580 else: 531 sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)581 _handle_sub_obj(related, sub_obj) 532 582 else: 533 583 # To make sure we can access all elements, we can't use the 534 584 # normal manager on the related object. So we work directly … … 541 591 raise AssertionError("Should never get here.") 542 592 delete_qs = rel_descriptor.delete_manager(self).all() 543 593 for sub_obj in delete_qs: 544 sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)594 _handle_sub_obj(related, sub_obj) 545 595 546 596 # Handle any ancestors (for the model-inheritance case). We do this by 547 597 # traversing to the most remote parent classes -- those with no parents … … 556 606 continue 557 607 # At this point, parent_obj is base class (no ancestor models). So 558 608 # delete it and all its descendents. 559 parent_obj._collect_sub_objects(seen_objs )609 parent_obj._collect_sub_objects(seen_objs, fields_to_set) 560 610 561 611 def delete(self): 562 612 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) 563 613 564 614 # Find all the objects than need to be deleted. 565 615 seen_objs = CollectedObjects() 566 self._collect_sub_objects(seen_objs) 616 fields_to_set = CollectedFields() 617 self._collect_sub_objects(seen_objs, fields_to_set) 567 618 568 619 # Actually delete the objects. 569 delete_objects(seen_objs) 570 620 delete_objects(seen_objs, fields_to_set) 571 621 delete.alters_data = True 572 622 573 623 def _get_FIELD_display(self, field): -
django/db/models/fields/related.py
20 20 21 21 pending_lookups = {} 22 22 23 class CASCADE(object): 24 pass 25 class PROTECT(object): 26 pass 27 class SET_NULL(object): 28 pass 29 class SET_DEFAULT(object): 30 pass 31 ALLOWED_ON_DELETE_ACTION_TYPES = set([None, CASCADE, PROTECT, SET_NULL, SET_DEFAULT]) 32 23 33 def add_lazy_relation(cls, field, relation, operation): 24 34 """ 25 35 Adds a lookup on ``cls`` when a related field is defined using a string, … … 218 228 # object you just set. 219 229 setattr(instance, self.cache_name, value) 220 230 setattr(value, self.related.field.get_cache_name(), instance) 231 232 #TODO: the following function is a bit of a hack 233 #It's one way (not a very good one) to work around stale cache data causing 234 #spurious RESTRICT errors, etc; it would be better to prevent the cache from 235 #becoming stale in the first place. 236 # def __delete__(self, instance): 237 # try: 238 # return delattr(instance, self.cache_name) 239 # except AttributeError: 240 # pass 221 241 222 242 class ReverseSingleRelatedObjectDescriptor(object): 223 243 # This class provides the functionality that makes the related-object … … 628 648 629 649 class ManyToOneRel(object): 630 650 def __init__(self, to, field_name, related_name=None, 631 limit_choices_to=None, lookup_overrides=None, parent_link=False): 651 limit_choices_to=None, lookup_overrides=None, parent_link=False, 652 on_delete=None): 632 653 try: 633 654 to._meta 634 655 except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT … … 641 662 self.lookup_overrides = lookup_overrides or {} 642 663 self.multiple = True 643 664 self.parent_link = parent_link 665 self.on_delete = on_delete 644 666 645 667 def get_related_field(self): 646 668 """ … … 655 677 656 678 class OneToOneRel(ManyToOneRel): 657 679 def __init__(self, to, field_name, related_name=None, 658 limit_choices_to=None, lookup_overrides=None, parent_link=False): 680 limit_choices_to=None, lookup_overrides=None, parent_link=False, 681 on_delete=None): 659 682 super(OneToOneRel, self).__init__(to, field_name, 660 683 related_name=related_name, limit_choices_to=limit_choices_to, 661 lookup_overrides=lookup_overrides, parent_link=parent_link) 684 lookup_overrides=lookup_overrides, parent_link=parent_link, 685 on_delete=on_delete) 662 686 self.multiple = False 663 687 664 688 class ManyToManyRel(object): … … 697 721 related_name=kwargs.pop('related_name', None), 698 722 limit_choices_to=kwargs.pop('limit_choices_to', None), 699 723 lookup_overrides=kwargs.pop('lookup_overrides', None), 700 parent_link=kwargs.pop('parent_link', False)) 724 parent_link=kwargs.pop('parent_link', False), 725 on_delete=kwargs.pop('on_delete', None)) 701 726 Field.__init__(self, **kwargs) 702 727 703 728 self.db_index = True … … 742 767 target = self.rel.to._meta.db_table 743 768 cls._meta.duplicate_targets[self.column] = (target, "o2m") 744 769 770 on_delete = self.rel.on_delete 771 if on_delete not in ALLOWED_ON_DELETE_ACTION_TYPES: 772 raise ValueError("Invalid value 'on_delete=%s' specified for %s %s.%s." % (on_delete, type(self).__name__, cls.__name__, name)) 773 if on_delete == SET_NULL and not self.null: 774 specification = "'on_delete=SET_NULL'" 775 raise ValueError("%s specified for %s '%s.%s', but the field is not nullable." % (specification, type(self).__name__, cls.__name__, name)) 776 if on_delete == SET_DEFAULT and not self.has_default(): 777 specification = "'on_delete=SET_DEFAULT'" 778 raise ValueError("%s specified for %s '%s.%s', but the field has no default value." % (specification, type(self).__name__, cls.__name__, name)) 779 745 780 def contribute_to_related_class(self, cls, related): 746 781 setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) 747 782 -
django/db/models/__init__.py
11 11 from django.db.models.fields.subclassing import SubfieldBase 12 12 from django.db.models.fields.files import FileField, ImageField 13 13 from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel 14 from django.db.models.fields.related import CASCADE, PROTECT, SET_NULL, SET_DEFAULT 14 15 from django.db.models import signals 15 16 16 17 # Admin stages. -
django/db/models/query.py
11 11 12 12 from django.db import connection, transaction, IntegrityError 13 13 from django.db.models.aggregates import Aggregate 14 from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE 14 15 from django.db.models.fields import DateField 15 from django.db.models.query_utils import Q, select_related_descend, Collected Objects, CyclicDependency, deferred_class_factory16 from django.db.models.query_utils import Q, select_related_descend, CollectedFields, CollectedObjects, CyclicDependency, deferred_class_factory 16 17 from django.db.models import signals, sql 17 18 18 19 … … 391 392 # Collect all the objects to be deleted in this chunk, and all the 392 393 # objects that are related to the objects that are to be deleted. 393 394 seen_objs = CollectedObjects(seen_objs) 395 fields_to_set = CollectedFields() 394 396 for object in del_query[:CHUNK_SIZE]: 395 object._collect_sub_objects(seen_objs )397 object._collect_sub_objects(seen_objs, fields_to_set) 396 398 397 399 if not seen_objs: 398 400 break 399 delete_objects(seen_objs )401 delete_objects(seen_objs, fields_to_set) 400 402 401 403 # Clear the result cache, in case this QuerySet gets reused. 402 404 self._result_cache = None … … 1002 1004 setattr(obj, f.get_cache_name(), rel_obj) 1003 1005 return obj, index_end 1004 1006 1005 def delete_objects(seen_objs ):1007 def delete_objects(seen_objs, fields_to_set): 1006 1008 """ 1007 1009 Iterate through a list of seen classes, and remove any instances that are 1008 1010 referred to. … … 1023 1025 1024 1026 obj_pairs = {} 1025 1027 try: 1028 for cls, cls_dct in fields_to_set.iteritems(): 1029 #TODO: batch these, similar to UpdateQuery.clear_related? 1030 #(Note that it may be harder to do here because the default value 1031 #for a given field may be different for each instance, 1032 #while UpdateQuery.clear_related always uses the value None). 1033 query = sql.UpdateQuery(cls, connection) 1034 for instance, field_names_and_values in cls_dct.itervalues(): 1035 query.where = query.where_class() 1036 pk = query.model._meta.pk 1037 query.where.add((sql.where.Constraint(None, pk.column, pk), 'exact', instance.pk), sql.where.AND) 1038 query.add_update_values(field_names_and_values) 1039 query.execute_sql() 1040 1026 1041 for cls in ordered_classes: 1027 1042 items = seen_objs[cls].items() 1028 1043 items.sort() … … 1032 1047 for pk_val, instance in items: 1033 1048 signals.pre_delete.send(sender=cls, instance=instance) 1034 1049 1050 # Handle related GenericRelation and ManyToManyField instances 1035 1051 pk_list = [pk for pk,instance in items] 1036 1052 del_query = sql.DeleteQuery(cls, connection) 1037 1053 del_query.delete_batch_related(pk_list) 1038 1054 1039 update_query = sql.UpdateQuery(cls, connection)1040 for field, model in cls._meta.get_fields_with_model():1041 if (field.rel and field.null and field.rel.to in seen_objs and1042 filter(lambda f: f.column == field.rel.get_related_field().column,1043 field.rel.to._meta.fields)):1044 if model:1045 sql.UpdateQuery(model, connection).clear_related(field,1046 pk_list)1047 else:1048 update_query.clear_related(field, pk_list)1049 1050 # Now delete the actual data.1051 1055 for cls in ordered_classes: 1052 1056 items = obj_pairs[cls] 1053 1057 items.reverse() 1054 1055 1058 pk_list = [pk for pk,instance in items] 1056 1059 del_query = sql.DeleteQuery(cls, connection) 1057 1060 del_query.delete_batch(pk_list) 1058 1061 1059 # Last cleanup; set NULLs where there once was a reference to the 1060 # object, NULL the primary key of the found objects, and perform 1061 # post-notification. 1062 #Last cleanup; set NULLs and default values where there once was a 1063 #reference to the object, NULL the primary key of the found objects, 1064 #and perform post-notification. 1065 for cls, cls_dct in fields_to_set.iteritems(): 1066 for instance, field_names_and_values in cls_dct.itervalues(): 1067 for field_name, field_value in field_names_and_values.iteritems(): 1068 field = cls._meta.get_field_by_name(field_name)[0] 1069 setattr(instance, field.attname, field_value) 1070 for cls in ordered_classes: 1071 items = obj_pairs[cls] 1072 items.reverse() 1062 1073 for pk_val, instance in items: 1063 1074 for field in cls._meta.fields: 1064 1075 if field.rel and field.null and field.rel.to in seen_objs: -
django/db/models/query_utils.py
124 124 """ 125 125 return self.data.keys() 126 126 127 class CollectedFields(object): 128 """ 129 A container that stores the model object and field name 130 for fields that need to be set to enforce on_delete=SET_NULL 131 and on_delete=SET_DEFAULT ForeigKey constraints. 132 """ 133 134 def __init__(self): 135 self.data = {} 136 137 def add(self, model, pk, obj, field_name, field_value): 138 """ 139 Adds an item. 140 model is the class of the object being added, 141 pk is the primary key, obj is the object itself, 142 field_name is the name of the field to be set, 143 field_value is the value it needs to be set to. 144 """ 145 d = self.data.setdefault(model, SortedDict()) 146 obj, field_names_and_values = d.setdefault(pk, (obj, dict())) 147 assert field_name not in field_names_and_values or field_names_and_values[field_name] == field_value 148 field_names_and_values[field_name] = field_value 149 150 def __contains__(self, key): 151 return self.data.__contains__(key) 152 153 def __getitem__(self, key): 154 return self.data[key] 155 156 def __nonzero__(self): 157 return bool(self.data) 158 159 def iteritems(self): 160 return self.data.iteritems() 161 162 def iterkeys(self): 163 return self.data.iterkeys() 164 165 def itervalues(self): 166 return self.data.itervalues() 167 168 def items(self): 169 return self.data.items() 170 171 def keys(self): 172 return self.data.keys() 173 174 def values(self): 175 return self.data.values() 176 127 177 class QueryWrapper(object): 128 178 """ 129 179 A type that indicates the contents are an SQL fragment and the associate -
tests/modeltests/delete/models.py
46 46 47 47 ## First, test the CollectedObjects data structure directly 48 48 49 >>> from django.db.models.query importCollectedObjects49 >>> from django.db.models.query_utils import CollectedFields, CollectedObjects 50 50 51 51 >>> g = CollectedObjects() 52 52 >>> g.add("key1", 1, "item1", None) … … 112 112 >>> d1 = D(c=c1, a=a1) 113 113 >>> d1.save() 114 114 115 >>> o = CollectedObjects()116 >>> a1._collect_sub_objects(o )115 >>> o, f = CollectedObjects(), CollectedFields() 116 >>> a1._collect_sub_objects(o, f) 117 117 >>> o.keys() 118 118 [<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>] 119 >>> f.keys() 120 [] 119 121 >>> a1.delete() 120 122 121 123 # Same again with a known bad order … … 131 133 >>> d2 = D(c=c2, a=a2) 132 134 >>> d2.save() 133 135 134 >>> o = CollectedObjects()135 >>> a2._collect_sub_objects(o )136 >>> o, f = CollectedObjects(), CollectedFields() 137 >>> a2._collect_sub_objects(o, f) 136 138 >>> o.keys() 137 139 [<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>] 140 >>> f.keys() 141 [] 138 142 >>> a2.delete() 139 143 140 144 ### Tests for models E,F - nullable related fields ### … … 163 167 # Since E.f is nullable, we should delete F first (after nulling out 164 168 # the E.f field), then E. 165 169 166 >>> o = CollectedObjects()167 >>> e1._collect_sub_objects(o )170 >>> o, f = CollectedObjects(), CollectedFields() 171 >>> e1._collect_sub_objects(o, f) 168 172 >>> o.keys() 169 173 [<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>] 174 >>> f.keys() 175 [<class 'modeltests.delete.models.E'>] 170 176 171 # temporarily replace the UpdateQuery class to verify that E.f is actually nulled out first172 >>> import django.db.models.sql173 >>> class LoggingUpdateQuery(django.db.models.sql.UpdateQuery):174 ... def clear_related(self, related_field, pk_list):175 ... print "CLEARING FIELD",related_field.name176 ... return super(LoggingUpdateQuery, self).clear_related(related_field, pk_list)177 >>> original_class = django.db.models.sql.UpdateQuery178 >>> django.db.models.sql.UpdateQuery = LoggingUpdateQuery179 177 >>> e1.delete() 180 CLEARING FIELD f181 178 182 179 >>> e2 = E() 183 180 >>> e2.save() … … 188 185 189 186 # Same deal as before, though we are starting from the other object. 190 187 191 >>> o = CollectedObjects()192 >>> f2._collect_sub_objects(o )188 >>> o, f = CollectedObjects(), CollectedFields() 189 >>> f2._collect_sub_objects(o, f) 193 190 >>> o.keys() 194 [<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>] 191 [<class 'modeltests.delete.models.F'>] 192 >>> f.keys() 193 [<class 'modeltests.delete.models.E'>] 195 194 196 195 >>> f2.delete() 197 CLEARING FIELD f198 199 # Put this back to normal200 >>> django.db.models.sql.UpdateQuery = original_class201 196 """ 202 197 }