Ticket #7539: on_delete_on_update-r10558.diff
File on_delete_on_update-r10558.diff, 20.9 KB (added by , 16 years ago) |
---|
-
db/models/base.py
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 CollectedObjects, DeferredAttribute16 from django.db.models.query_utils import CollectedObjects, CollectedFields, 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, RESTRICT, SET_NULL 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 … … 495 496 496 497 save_base.alters_data = True 497 498 498 def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):499 def _collect_sub_objects(self, seen_objs, fields_to_null, parent=None, nullable=False): 499 500 """ 500 501 Recursively populates seen_objs with all objects related to this 501 502 object. … … 508 509 if seen_objs.add(self.__class__, pk_val, self, parent, nullable): 509 510 return 510 511 511 for related in self._meta.get_all_related_objects(): 512 rel_opts_name = related.get_accessor_name() 513 if isinstance(related.field.rel, OneToOneRel): 514 try: 515 sub_obj = getattr(self, rel_opts_name) 516 except ObjectDoesNotExist: 517 pass 512 if not getattr(settings, 'ON_DELETE_HANDLED_BY_DB', False): 513 ON_DELETE_NONE_HANDLING = getattr(settings, 'ON_DELETE_NONE_HANDLING', CASCADE) 514 515 def _handle_sub_obj(related, sub_obj): 516 on_delete = related.field.rel.on_delete 517 if on_delete is None: 518 on_delete = ON_DELETE_NONE_HANDLING 519 520 if on_delete == CASCADE: 521 sub_obj._collect_sub_objects(seen_objs, fields_to_null, self.__class__) 522 elif on_delete == RESTRICT: 523 msg = '[Django] Cannot delete a parent object: a foreign key constraint fails (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % ( 524 sub_obj.__class__, 525 sub_obj._get_pk_val(), 526 self.__class__, 527 pk_val, 528 ) 529 raise IntegrityError(msg) #TODO: include error number also? E.g., "raise IntegrityError(1451, msg)" (but 1451 is MySQL-specific, I believe) 530 elif on_delete == SET_NULL: 531 if not related.field.null: 532 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`))' % ( 533 sub_obj.__class__, 534 sub_obj._get_pk_val(), 535 self.__class__, 536 pk_val, 537 ) 538 raise IntegrityError(msg) #TODO: include error number also? E.g., "raise IntegrityError(<errno>, msg)" (but the error numbers are db-specific, I believe) 539 fields_to_null.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name) 518 540 else: 519 sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null) 520 else: 521 # To make sure we can access all elements, we can't use the 522 # normal manager on the related object. So we work directly 523 # with the descriptor object. 524 for cls in self.__class__.mro(): 525 if rel_opts_name in cls.__dict__: 526 rel_descriptor = cls.__dict__[rel_opts_name] 527 break 541 raise AttributeError('Unexpected value for on_delete') 542 543 for related in self._meta.get_all_related_objects(): 544 rel_opts_name = related.get_accessor_name() 545 if isinstance(related.field.rel, OneToOneRel): 546 try: 547 sub_obj = getattr(self, rel_opts_name) 548 except ObjectDoesNotExist: 549 pass 550 else: 551 _handle_sub_obj(related, sub_obj) 528 552 else: 529 raise AssertionError("Should never get here.") 530 delete_qs = rel_descriptor.delete_manager(self).all() 531 for sub_obj in delete_qs: 532 sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null) 553 # To make sure we can access all elements, we can't use the 554 # normal manager on the related object. So we work directly 555 # with the descriptor object. 556 for cls in self.__class__.mro(): 557 if rel_opts_name in cls.__dict__: 558 rel_descriptor = cls.__dict__[rel_opts_name] 559 break 560 else: 561 raise AssertionError("Should never get here.") 562 delete_qs = rel_descriptor.delete_manager(self).all() 563 for sub_obj in delete_qs: 564 _handle_sub_obj(related, sub_obj) 533 565 534 # Handle any ancestors (for the model-inheritance case). We do this by535 # traversing to the most remote parent classes -- those with no parents536 # themselves -- and then adding those instances to the collection. That537 # will include all the child instances down to "self".538 parent_stack = self._meta.parents.values()539 while parent_stack:540 link = parent_stack.pop()541 parent_obj = getattr(self, link.name)542 if parent_obj._meta.parents:543 parent_stack.extend(parent_obj._meta.parents.values())544 continue545 # At this point, parent_obj is base class (no ancestor models). So546 # delete it and all its descendents.547 parent_obj._collect_sub_objects(seen_objs)566 # Handle any ancestors (for the model-inheritance case). We do this by 567 # traversing to the most remote parent classes -- those with no parents 568 # themselves -- and then adding those instances to the collection. That 569 # will include all the child instances down to "self". 570 parent_stack = self._meta.parents.values() 571 while parent_stack: 572 link = parent_stack.pop() 573 parent_obj = getattr(self, link.name) 574 if parent_obj._meta.parents: 575 parent_stack.extend(parent_obj._meta.parents.values()) 576 continue 577 # At this point, parent_obj is base class (no ancestor models). So 578 # delete it and all its descendents. 579 parent_obj._collect_sub_objects(seen_objs, fields_to_null) 548 580 549 581 def delete(self): 550 582 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) 551 583 552 584 # Find all the objects than need to be deleted. 553 585 seen_objs = CollectedObjects() 554 self._collect_sub_objects(seen_objs) 586 fields_to_null = CollectedFields() 587 self._collect_sub_objects(seen_objs, fields_to_null) 555 588 556 589 # Actually delete the objects. 557 delete_objects(seen_objs )590 delete_objects(seen_objs, fields_to_null) 558 591 559 592 delete.alters_data = True 560 593 -
db/models/fields/related.py
10 10 from django.utils.functional import curry 11 11 from django.core import exceptions 12 12 from django import forms 13 from django.conf import settings 13 14 14 15 try: 15 16 set … … 20 21 21 22 pending_lookups = {} 22 23 24 class _OnDeleteOrUpdateAction(object): 25 pass 26 class RESTRICT(_OnDeleteOrUpdateAction): 27 sql = 'RESTRICT' 28 class CASCADE(_OnDeleteOrUpdateAction): 29 sql = 'CASCADE' 30 class SET_NULL(_OnDeleteOrUpdateAction): 31 sql = 'SET NULL' 32 ALLOWED_ON_DELETE_ACTION_TYPES = set([None, CASCADE, RESTRICT, SET_NULL]) 33 ALLOWED_ON_UPDATE_ACTION_TYPES = ALLOWED_ON_DELETE_ACTION_TYPES.copy() 34 23 35 def add_lazy_relation(cls, field, relation, operation): 24 36 """ 25 37 Adds a lookup on ``cls`` when a related field is defined using a string, … … 604 616 605 617 class ManyToOneRel(object): 606 618 def __init__(self, to, field_name, related_name=None, 607 limit_choices_to=None, lookup_overrides=None, parent_link=False): 619 limit_choices_to=None, lookup_overrides=None, parent_link=False, 620 on_delete=None, on_update=None): 608 621 try: 609 622 to._meta 610 623 except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT … … 617 630 self.lookup_overrides = lookup_overrides or {} 618 631 self.multiple = True 619 632 self.parent_link = parent_link 633 self.on_delete = on_delete 634 self.on_update = on_update 620 635 621 636 def get_related_field(self): 622 637 """ … … 631 646 632 647 class OneToOneRel(ManyToOneRel): 633 648 def __init__(self, to, field_name, related_name=None, 634 limit_choices_to=None, lookup_overrides=None, parent_link=False): 649 limit_choices_to=None, lookup_overrides=None, parent_link=False, 650 on_delete=None, on_update=None): 635 651 super(OneToOneRel, self).__init__(to, field_name, 636 652 related_name=related_name, limit_choices_to=limit_choices_to, 637 lookup_overrides=lookup_overrides, parent_link=parent_link) 653 lookup_overrides=lookup_overrides, parent_link=parent_link, 654 on_delete=on_delete, on_update=on_update) 638 655 self.multiple = False 639 656 640 657 class ManyToManyRel(object): … … 673 690 related_name=kwargs.pop('related_name', None), 674 691 limit_choices_to=kwargs.pop('limit_choices_to', None), 675 692 lookup_overrides=kwargs.pop('lookup_overrides', None), 676 parent_link=kwargs.pop('parent_link', False)) 693 parent_link=kwargs.pop('parent_link', False), 694 on_delete=kwargs.pop('on_delete', None), 695 on_update=kwargs.pop('on_update', None)) 677 696 Field.__init__(self, **kwargs) 678 697 679 698 self.db_index = True … … 718 737 target = self.rel.to._meta.db_table 719 738 cls._meta.duplicate_targets[self.column] = (target, "o2m") 720 739 740 on_delete, on_update = self.rel.on_delete, self.rel.on_update 741 if on_delete not in ALLOWED_ON_DELETE_ACTION_TYPES: 742 raise ValueError("Invalid value 'on_delete=%s' specified for ForeignKey field %s.%s." % (on_delete, cls.__name__, name)) 743 if on_update not in ALLOWED_ON_UPDATE_ACTION_TYPES: 744 raise ValueError("Invalid value 'on_update=%s' specified for ForeignKey field %s.%s." % (on_update, cls.__name__, name)) 745 if (on_delete == SET_NULL or on_update == SET_NULL) and not self.null: 746 if on_delete == SET_NULL and on_update == SET_NULL: 747 specification = "'on_delete=SET_NULL' and 'on_update=SET_NULL'" 748 elif on_delete == SET_NULL: 749 specification = "'on_delete=SET_NULL'" 750 else: 751 specification = "'on_update=SET_NULL'" 752 raise ValueError("%s specified for ForeignKey field '%s.%s', but the field is not nullable." % (specification, cls.__name__, name)) 753 if on_delete is None and getattr(settings, 'ON_DELETE_NONE_HANDLING', CASCADE) == SET_NULL and not self.null: 754 raise ValueError("on_delete=SET_NULL is being used by default (based on the ON_DELETE_NONE_HANDLING setting) for ForeignKey field '%s.%s', but the field is not nullable." % (specification, cls.__name__, name)) 755 721 756 def contribute_to_related_class(self, cls, related): 722 757 setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) 723 758 -
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, RESTRICT, SET_NULL 14 15 from django.db.models import signals 15 16 16 17 # Admin stages. -
db/models/query.py
359 359 # Collect all the objects to be deleted in this chunk, and all the 360 360 # objects that are related to the objects that are to be deleted. 361 361 seen_objs = CollectedObjects() 362 fields_to_null = CollectedFields() 362 363 for object in del_query[:CHUNK_SIZE]: 363 object._collect_sub_objects(seen_objs )364 object._collect_sub_objects(seen_objs, fields_to_null) 364 365 365 366 if not seen_objs: 366 367 break 367 delete_objects(seen_objs )368 delete_objects(seen_objs, fields_to_null) 368 369 369 370 # Clear the result cache, in case this QuerySet gets reused. 370 371 self._result_cache = None … … 952 953 setattr(obj, f.get_cache_name(), rel_obj) 953 954 return obj, index_end 954 955 955 def delete_objects(seen_objs ):956 def delete_objects(seen_objs, fields_to_null): 956 957 """ 957 958 Iterate through a list of seen classes, and remove any instances that are 958 959 referred to. … … 998 999 update_query.clear_related(field, pk_list) 999 1000 1000 1001 # Now delete the actual data. 1002 for cls, cls_dct in fields_to_null.iteritems(): 1003 update_query = sql.UpdateQuery(cls, connection) 1004 field_dict = {} 1005 for pk, (_, field_names) in cls_dct.iteritems(): 1006 for field_name in field_names: 1007 pk_set = field_dict.setdefault(field_name, set()) 1008 pk_set.add(pk) 1009 for field_name, pk_set in field_dict.iteritems(): 1010 update_query.clear_related(cls._meta.get_field_by_name(field_name)[0], list(pk_set)) 1001 1011 for cls in ordered_classes: 1002 1012 items = obj_pairs[cls] 1003 1013 items.reverse() … … 1009 1019 # Last cleanup; set NULLs where there once was a reference to the 1010 1020 # object, NULL the primary key of the found objects, and perform 1011 1021 # post-notification. 1022 for cls, cls_dct in fields_to_null.iteritems(): 1023 for instance, field_names in cls_dct.itervalues(): 1024 for field_name in field_names: 1025 field = cls._meta.get_field_by_name(field_name)[0] 1026 setattr(instance, field.attname, None) 1012 1027 for pk_val, instance in items: 1013 1028 for field in cls._meta.fields: 1014 1029 if field.rel and field.null and field.rel.to in seen_objs: -
db/models/query_utils.py
111 111 """ 112 112 return self.data.keys() 113 113 114 class CollectedFields(object): 115 """ 116 A container that stores model objects and fields that need to 117 be nulled to enforce the on_delete=SET_NULL ForeignKey constraint. 118 """ 119 def __init__(self): 120 self.data = {} 121 122 def add(self, model, pk, obj, field_name): 123 """ 124 Adds an item. 125 model is the class of the object being added, 126 pk is the primary key, obj is the object itself, 127 field_name is the name of the field to be nulled. 128 """ 129 d = self.data.setdefault(model, SortedDict()) 130 obj, field_names = d.setdefault(pk, (obj, set())) 131 field_names.add(field_name) 132 133 def __contains__(self, key): 134 return self.data.__contains__(key) 135 136 def __getitem__(self, key): 137 return self.data[key] 138 139 def __nonzero__(self): 140 return bool(self.data) 141 142 def iteritems(self): 143 return self.data.iteritems() 144 145 def iterkeys(self): 146 return self.data.iterkeys() 147 148 def itervalues(self): 149 return self.data.itervalues() 150 114 151 class QueryWrapper(object): 115 152 """ 116 153 A type that indicates the contents are an SQL fragment and the associate -
db/backends/creation.py
96 96 "Return the SQL snippet defining the foreign key reference for a field" 97 97 qn = self.connection.ops.quote_name 98 98 if field.rel.to in known_models: 99 on_delete_clause, on_update_clause = self.on_delete_and_update_clauses(style, field) 99 100 output = [style.SQL_KEYWORD('REFERENCES') + ' ' + \ 100 101 style.SQL_TABLE(qn(field.rel.to._meta.db_table)) + ' (' + \ 101 102 style.SQL_FIELD(qn(field.rel.to._meta.get_field(field.rel.field_name).column)) + ')' + 103 on_delete_clause + on_update_clause + 102 104 self.connection.ops.deferrable_sql() 103 105 ] 104 106 pending = False … … 121 123 opts = model._meta 122 124 if model in pending_references: 123 125 for rel_class, f in pending_references[model]: 126 on_delete_clause, on_update_clause = self.on_delete_and_update_clauses(style, f) 124 127 rel_opts = rel_class._meta 125 128 r_table = rel_opts.db_table 126 129 r_col = f.column … … 129 132 # For MySQL, r_name must be unique in the first 64 characters. 130 133 # So we are careful with character usage here. 131 134 r_name = '%s_refs_%s_%x' % (r_col, col, abs(hash((r_table, table)))) 132 final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s ;' % \135 final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s%s%s;' % \ 133 136 (qn(r_table), qn(truncate_name(r_name, self.connection.ops.max_name_length())), 134 137 qn(r_col), qn(table), qn(col), 138 on_delete_clause, on_update_clause, 135 139 self.connection.ops.deferrable_sql())) 136 140 del pending_references[model] 137 141 return final_output 138 142 143 def on_delete_and_update_clauses(self, style, foreign_key_field): 144 on_delete_clause = '' 145 try: 146 on_delete = getattr(foreign_key_field, 'ON_DELETE_HANDLED_BY_DB') #USED FOR UNIT TESTING ONLY (see comment in modeltests/on_delete_and_update_db/models.py) 147 except AttributeError: 148 on_delete = getattr(settings, 'ON_DELETE_HANDLED_BY_DB', False) 149 if on_delete: 150 on_delete = getattr(foreign_key_field.rel, 'on_delete', None) 151 if on_delete: 152 on_delete_clause = style.SQL_KEYWORD(' ON DELETE %s' % foreign_key_field.rel.on_delete.sql) 153 154 on_update_clause = '' 155 try: 156 on_update = getattr(foreign_key_field, 'ON_UPDATE_HANDLED_BY_DB') #USED FOR UNIT TESTING ONLY; (see comment in modeltests/on_delete_and_update_db/models.py) 157 except AttributeError: 158 on_update = getattr(settings, 'ON_UPDATE_HANDLED_BY_DB', False) 159 if on_update: 160 on_update = getattr(foreign_key_field.rel, 'on_update', None) 161 if on_update: 162 on_update_clause = style.SQL_KEYWORD(' ON UPDATE %s' % foreign_key_field.rel.on_update.sql) 163 164 return on_delete_clause, on_update_clause 165 139 166 def sql_for_many_to_many(self, model, style): 140 167 "Return the CREATE TABLE statments for all the many-to-many tables defined on a model" 141 168 output = [] -
contrib/admin/options.py
1006 1006 perms_needed = set() 1007 1007 get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site) 1008 1008 1009 import logging; logging.debug('deleting %s' % deleted_objects) 1009 1010 if request.POST: # The user has already confirmed the deletion. 1010 1011 if perms_needed: 1011 1012 raise PermissionDenied