Ticket #7539: on_delete_on_update-r10558.diff

File on_delete_on_update-r10558.diff, 20.9 KB (added by Mathijs de Bruin, 15 years ago)

Patch updated for r10558. Very fresh and barely tested, see more info in ticket.

  • db/models/base.py

     
    1313from django.db.models.fields import AutoField, FieldDoesNotExist
    1414from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
    1515from django.db.models.query import delete_objects, Q
    16 from django.db.models.query_utils import CollectedObjects, DeferredAttribute
     16from django.db.models.query_utils import CollectedObjects, CollectedFields, DeferredAttribute
    1717from django.db.models.options import Options
    18 from django.db import connection, transaction, DatabaseError
     18from django.db import connection, transaction, DatabaseError, IntegrityError
    1919from django.db.models import signals
     20from django.db.models.fields.related import CASCADE, RESTRICT, SET_NULL
    2021from django.db.models.loading import register_models, get_model
    2122from django.utils.functional import curry
    2223from django.utils.encoding import smart_str, force_unicode, smart_unicode
     
    495496
    496497    save_base.alters_data = True
    497498
    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):
    499500        """
    500501        Recursively populates seen_objs with all objects related to this
    501502        object.
     
    508509        if seen_objs.add(self.__class__, pk_val, self, parent, nullable):
    509510            return
    510511
    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)
    518540                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)
    528552                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)
    533565
    534         # Handle any ancestors (for the model-inheritance case). We do this by
    535         # traversing to the most remote parent classes -- those with no parents
    536         # themselves -- and then adding those instances to the collection. That
    537         # 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                 continue
    545             # At this point, parent_obj is base class (no ancestor models). So
    546             # 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)
    548580
    549581    def delete(self):
    550582        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)
    551583
    552584        # Find all the objects than need to be deleted.
    553585        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)
    555588
    556589        # Actually delete the objects.
    557         delete_objects(seen_objs)
     590        delete_objects(seen_objs, fields_to_null)
    558591
    559592    delete.alters_data = True
    560593
  • db/models/fields/related.py

     
    1010from django.utils.functional import curry
    1111from django.core import exceptions
    1212from django import forms
     13from django.conf import settings
    1314
    1415try:
    1516    set
     
    2021
    2122pending_lookups = {}
    2223
     24class _OnDeleteOrUpdateAction(object):
     25    pass
     26class RESTRICT(_OnDeleteOrUpdateAction):
     27    sql = 'RESTRICT'
     28class CASCADE(_OnDeleteOrUpdateAction):
     29    sql = 'CASCADE'
     30class SET_NULL(_OnDeleteOrUpdateAction):
     31    sql = 'SET NULL'
     32ALLOWED_ON_DELETE_ACTION_TYPES = set([None, CASCADE, RESTRICT, SET_NULL])
     33ALLOWED_ON_UPDATE_ACTION_TYPES = ALLOWED_ON_DELETE_ACTION_TYPES.copy()
     34
    2335def add_lazy_relation(cls, field, relation, operation):
    2436    """
    2537    Adds a lookup on ``cls`` when a related field is defined using a string,
     
    604616
    605617class ManyToOneRel(object):
    606618    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):
    608621        try:
    609622            to._meta
    610623        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
     
    617630        self.lookup_overrides = lookup_overrides or {}
    618631        self.multiple = True
    619632        self.parent_link = parent_link
     633        self.on_delete = on_delete
     634        self.on_update = on_update
    620635
    621636    def get_related_field(self):
    622637        """
     
    631646
    632647class OneToOneRel(ManyToOneRel):
    633648    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):
    635651        super(OneToOneRel, self).__init__(to, field_name,
    636652                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)
    638655        self.multiple = False
    639656
    640657class ManyToManyRel(object):
     
    673690            related_name=kwargs.pop('related_name', None),
    674691            limit_choices_to=kwargs.pop('limit_choices_to', None),
    675692            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))
    677696        Field.__init__(self, **kwargs)
    678697
    679698        self.db_index = True
     
    718737            target = self.rel.to._meta.db_table
    719738        cls._meta.duplicate_targets[self.column] = (target, "o2m")
    720739
     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
    721756    def contribute_to_related_class(self, cls, related):
    722757        setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
    723758
  • 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, RESTRICT, SET_NULL
    1415from django.db.models import signals
    1516
    1617# Admin stages.
  • db/models/query.py

     
    359359            # Collect all the objects to be deleted in this chunk, and all the
    360360            # objects that are related to the objects that are to be deleted.
    361361            seen_objs = CollectedObjects()
     362            fields_to_null = CollectedFields()
    362363            for object in del_query[:CHUNK_SIZE]:
    363                 object._collect_sub_objects(seen_objs)
     364                object._collect_sub_objects(seen_objs, fields_to_null)
    364365
    365366            if not seen_objs:
    366367                break
    367             delete_objects(seen_objs)
     368            delete_objects(seen_objs, fields_to_null)
    368369
    369370        # Clear the result cache, in case this QuerySet gets reused.
    370371        self._result_cache = None
     
    952953                setattr(obj, f.get_cache_name(), rel_obj)
    953954    return obj, index_end
    954955
    955 def delete_objects(seen_objs):
     956def delete_objects(seen_objs, fields_to_null):
    956957    """
    957958    Iterate through a list of seen classes, and remove any instances that are
    958959    referred to.
     
    998999                        update_query.clear_related(field, pk_list)
    9991000
    10001001        # 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))
    10011011        for cls in ordered_classes:
    10021012            items = obj_pairs[cls]
    10031013            items.reverse()
     
    10091019            # Last cleanup; set NULLs where there once was a reference to the
    10101020            # object, NULL the primary key of the found objects, and perform
    10111021            # 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)
    10121027            for pk_val, instance in items:
    10131028                for field in cls._meta.fields:
    10141029                    if field.rel and field.null and field.rel.to in seen_objs:
  • db/models/query_utils.py

     
    111111        """
    112112        return self.data.keys()
    113113
     114class 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
    114151class QueryWrapper(object):
    115152    """
    116153    A type that indicates the contents are an SQL fragment and the associate
  • db/backends/creation.py

     
    9696        "Return the SQL snippet defining the foreign key reference for a field"
    9797        qn = self.connection.ops.quote_name
    9898        if field.rel.to in known_models:
     99            on_delete_clause, on_update_clause = self.on_delete_and_update_clauses(style, field)
    99100            output = [style.SQL_KEYWORD('REFERENCES') + ' ' + \
    100101                style.SQL_TABLE(qn(field.rel.to._meta.db_table)) + ' (' + \
    101102                style.SQL_FIELD(qn(field.rel.to._meta.get_field(field.rel.field_name).column)) + ')' +
     103                on_delete_clause + on_update_clause +
    102104                self.connection.ops.deferrable_sql()
    103105            ]
    104106            pending = False
     
    121123        opts = model._meta
    122124        if model in pending_references:
    123125            for rel_class, f in pending_references[model]:
     126                on_delete_clause, on_update_clause = self.on_delete_and_update_clauses(style, f)
    124127                rel_opts = rel_class._meta
    125128                r_table = rel_opts.db_table
    126129                r_col = f.column
     
    129132                # For MySQL, r_name must be unique in the first 64 characters.
    130133                # So we are careful with character usage here.
    131134                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;' % \
    133136                    (qn(r_table), qn(truncate_name(r_name, self.connection.ops.max_name_length())),
    134137                    qn(r_col), qn(table), qn(col),
     138                    on_delete_clause, on_update_clause,
    135139                    self.connection.ops.deferrable_sql()))
    136140            del pending_references[model]
    137141        return final_output
    138142
     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       
    139166    def sql_for_many_to_many(self, model, style):
    140167        "Return the CREATE TABLE statments for all the many-to-many tables defined on a model"
    141168        output = []
  • contrib/admin/options.py

     
    10061006        perms_needed = set()
    10071007        get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
    10081008
     1009        import logging; logging.debug('deleting %s' % deleted_objects)
    10091010        if request.POST: # The user has already confirmed the deletion.
    10101011            if perms_needed:
    10111012                raise PermissionDenied
Back to Top