| from functools import wraps |
| from operator import attrgetter |
| |
| from django.db import connections, transaction, IntegrityError |
| from django.db.models import signals, sql |
| from django.utils.datastructures import SortedDict |
| from django.utils import six |
| |
| |
| class ProtectedError(IntegrityError): |
| def __init__(self, msg, protected_objects): |
| self.protected_objects = protected_objects |
| super(ProtectedError, self).__init__(msg, protected_objects) |
| |
| |
| def CASCADE(collector, field, sub_objs, using): |
| collector.collect(sub_objs, source=field.rel.to, |
| source_attr=field.name, nullable=field.null) |
| if field.null and not connections[using].features.can_defer_constraint_checks: |
| collector.add_field_update(field, None, sub_objs) |
| |
| |
| def PROTECT(collector, field, sub_objs, using): |
| raise ProtectedError("Cannot delete some instances of model '%s' because " |
| "they are referenced through a protected foreign key: '%s.%s'" % ( |
| field.rel.to.__name__, sub_objs[0].__class__.__name__, field.name |
| ), |
| sub_objs |
| ) |
| |
| |
| def SET(value): |
| if callable(value): |
| def set_on_delete(collector, field, sub_objs, using): |
| collector.add_field_update(field, value(), sub_objs) |
| else: |
| def set_on_delete(collector, field, sub_objs, using): |
| collector.add_field_update(field, value, sub_objs) |
| return set_on_delete |
| |
| |
| SET_NULL = SET(None) |
| |
| |
| def SET_DEFAULT(collector, field, sub_objs, using): |
| collector.add_field_update(field, field.get_default(), sub_objs) |
| |
| |
| def DO_NOTHING(collector, field, sub_objs, using): |
| pass |
| |
| |
| def force_managed(func): |
| @wraps(func) |
| def decorated(self, *args, **kwargs): |
| if not transaction.is_managed(using=self.using): |
| transaction.enter_transaction_management(using=self.using) |
| forced_managed = True |
| else: |
| forced_managed = False |
| try: |
| func(self, *args, **kwargs) |
| if forced_managed: |
| transaction.commit(using=self.using) |
| else: |
| transaction.commit_unless_managed(using=self.using) |
| finally: |
| if forced_managed: |
| transaction.leave_transaction_management(using=self.using) |
| return decorated |
| |
| |
| class Collector(object): |
| def __init__(self, using): |
| self.using = using |
| # Initially, {model: set([instances])}, later values become lists. |
| self.data = {} |
| self.batches = {} # {model: {field: set([instances])}} |
| self.field_updates = {} # {model: {(field, value): set([instances])}} |
| # fast_deletes is a list of queryset-likes that can be deleted without |
| # fetching the objects into memory. |
| self.fast_deletes = [] |
| |
| # Tracks deletion-order dependency for databases without transactions |
| # or ability to defer constraint checks. Only concrete model classes |
| # should be included, as the dependencies exist only between actual |
| # database tables; proxy models are represented here by their concrete |
| # parent. |
| self.dependencies = {} # {model: set([models])} |
| |
| def add(self, objs, source=None, nullable=False, reverse_dependency=False): |
| """ |
| Adds 'objs' to the collection of objects to be deleted. If the call is |
| the result of a cascade, 'source' should be the model that caused it, |
| and 'nullable' should be set to True if the relation can be null. |
| |
| Returns a list of all objects that were not already collected. |
| """ |
| if not objs: |
| return [] |
| new_objs = [] |
| model = objs[0].__class__ |
| instances = self.data.setdefault(model, set()) |
| for obj in objs: |
| if obj not in instances: |
| new_objs.append(obj) |
| instances.update(new_objs) |
| # Nullable relationships can be ignored -- they are nulled out before |
| # deleting, and therefore do not affect the order in which objects have |
| # to be deleted. |
| if source is not None and not nullable: |
| if reverse_dependency: |
| source, model = model, source |
| self.dependencies.setdefault( |
| source._meta.concrete_model, set()).add(model._meta.concrete_model) |
| return new_objs |
| |
| def add_batch(self, model, field, objs): |
| """ |
| Schedules a batch delete. Every instance of 'model' that is related to |
| an instance of 'obj' through 'field' will be deleted. |
| """ |
| self.batches.setdefault(model, {}).setdefault(field, set()).update(objs) |
| |
| def add_field_update(self, field, value, objs): |
| """ |
| Schedules a field update. 'objs' must be a homogenous iterable |
| collection of model instances (e.g. a QuerySet). |
| """ |
| if not objs: |
| return |
| model = objs[0].__class__ |
| self.field_updates.setdefault( |
| model, {}).setdefault( |
| (field, value), set()).update(objs) |
| |
| def can_fast_delete(self, objs, from_field=None): |
| """ |
| Determines if the objects in the given queryset-like can be |
| fast-deleted. This can be done if there are no cascades, no |
| parents and no signal listeners for the object class. |
| |
| The 'from_field' tells where we are coming from - we need this to |
| determine if the objects are in fact to be deleted. Allows also |
| skipping parent -> child -> parent chain preventing fast delete of |
| the child. |
| """ |
| if from_field and from_field.rel.on_delete is not CASCADE: |
| return False |
| if not (hasattr(objs, 'model') and hasattr(objs, '_raw_delete')): |
| return False |
| model = objs.model |
| if (signals.pre_delete.has_listeners(model) |
| or signals.post_delete.has_listeners(model) |
| or signals.m2m_changed.has_listeners(model)): |
| return False |
| # The use of from_field comes from the need to avoid cascade back to |
| # parent when parent delete is cascading to child. |
| opts = model._meta |
| if any(link != from_field for link in opts.concrete_model._meta.parents.values()): |
| return False |
| # Foreign keys pointing to this model, both from m2m and other |
| # models. |
| for related in opts.get_all_related_objects( |
| include_hidden=True, include_proxy_eq=True): |
| if related.field.rel.on_delete is not DO_NOTHING: |
| return False |
| # GFK deletes |
| for relation in opts.many_to_many: |
| if not relation.rel.through: |
| return False |
| return True |
| |
| def collect(self, objs, source=None, nullable=False, collect_related=True, |
| source_attr=None, reverse_dependency=False): |
| """ |
| Adds 'objs' to the collection of objects to be deleted as well as all |
| parent instances. 'objs' must be a homogenous iterable collection of |
| model instances (e.g. a QuerySet). If 'collect_related' is True, |
| related objects will be handled by their respective on_delete handler. |
| |
| If the call is the result of a cascade, 'source' should be the model |
| that caused it and 'nullable' should be set to True, if the relation |
| can be null. |
| |
| If 'reverse_dependency' is True, 'source' will be deleted before the |
| current model, rather than after. (Needed for cascading to parent |
| models, the one case in which the cascade follows the forwards |
| direction of an FK rather than the reverse direction.) |
| """ |
| if self.can_fast_delete(objs): |
| self.fast_deletes.append(objs) |
| return |
| new_objs = self.add(objs, source, nullable, |
| reverse_dependency=reverse_dependency) |
| if not new_objs: |
| return |
| |
| model = new_objs[0].__class__ |
| |
| # Recursively collect concrete model's parent models, but not their |
| # related objects. These will be found by meta.get_all_related_objects() |
| concrete_model = model._meta.concrete_model |
| for ptr in six.itervalues(concrete_model._meta.parents): |
| if ptr: |
| # FIXME: This seems to be buggy and execute a query for each |
| # parent object fetch. We have the parent data in the obj, |
| # but we don't have a nice way to turn that data into parent |
| # object instance. |
| parent_objs = [getattr(obj, ptr.name) for obj in new_objs] |
| self.collect(parent_objs, source=model, |
| source_attr=ptr.rel.related_name, |
| collect_related=False, |
| reverse_dependency=True) |
| |
| if collect_related: |
| for related in model._meta.get_all_related_objects( |
| include_hidden=True, include_proxy_eq=True): |
| field = related.field |
| if field.rel.on_delete == DO_NOTHING: |
| continue |
| sub_objs = self.related_objects(related, new_objs) |
| if self.can_fast_delete(sub_objs, from_field=field): |
| self.fast_deletes.append(sub_objs) |
| elif sub_objs: |
| field.rel.on_delete(self, field, sub_objs, self.using) |
| |
| # TODO This entire block is only needed as a special case to |
| # support cascade-deletes for GenericRelation. It should be |
| # removed/fixed when the ORM gains a proper abstraction for virtual |
| # or composite fields, and GFKs are reworked to fit into that. |
| for relation in model._meta.many_to_many: |
| if not relation.rel.through: |
| sub_objs = relation.bulk_related_objects(new_objs, self.using) |
| self.collect(sub_objs, |
| source=model, |
| source_attr=relation.rel.related_name, |
| nullable=True) |
| |
| def related_objects(self, related, objs): |
| """ |
| Gets a QuerySet of objects related to ``objs`` via the relation ``related``. |
| |
| """ |
| return related.model._base_manager.using(self.using).filter( |
| **{"%s__in" % related.field.name: objs} |
| ) |
| |
| def instances_with_model(self): |
| for model, instances in six.iteritems(self.data): |
| for obj in instances: |
| yield model, obj |
| |
| def sort(self): |
| sorted_models = [] |
| concrete_models = set() |
| models = list(self.data) |
| while len(sorted_models) < len(models): |
| found = False |
| for model in models: |
| if model in sorted_models: |
| continue |
| dependencies = self.dependencies.get(model._meta.concrete_model) |
| if not (dependencies and dependencies.difference(concrete_models)): |
| sorted_models.append(model) |
| concrete_models.add(model._meta.concrete_model) |
| found = True |
| if not found: |
| return |
| self.data = SortedDict([(model, self.data[model]) |
| for model in sorted_models]) |
| |
| @force_managed |
| def delete(self): |
| # sort instance collections |
| for model, instances in self.data.items(): |
| self.data[model] = sorted(instances, key=attrgetter("pk")) |
| |
| # if possible, bring the models in an order suitable for databases that |
| # don't support transactions or cannot defer constraint checks until the |
| # end of a transaction. |
| self.sort() |
| |
| # send pre_delete signals |
| for model, obj in self.instances_with_model(): |
| if not model._meta.auto_created: |
| signals.pre_delete.send( |
| sender=model, instance=obj, using=self.using |
| ) |
| |
| # fast deletes |
| for qs in self.fast_deletes: |
| qs._raw_delete(using=self.using) |
| |
| # update fields |
| for model, instances_for_fieldvalues in six.iteritems(self.field_updates): |
| query = sql.UpdateQuery(model) |
| for (field, value), instances in six.iteritems(instances_for_fieldvalues): |
| query.update_batch([obj.pk for obj in instances], |
| {field.name: value}, self.using) |
| |
| # reverse instance collections |
| for instances in six.itervalues(self.data): |
| instances.reverse() |
| |
| # delete batches |
| for model, batches in six.iteritems(self.batches): |
| query = sql.DeleteQuery(model) |
| for field, instances in six.iteritems(batches): |
| query.delete_batch([obj.pk for obj in instances], self.using, field) |
| |
| # delete instances |
| for model, instances in six.iteritems(self.data): |
| query = sql.DeleteQuery(model) |
| pk_list = [obj.pk for obj in instances] |
| query.delete_batch(pk_list, self.using) |
| |
| # send post_delete signals |
| for model, obj in self.instances_with_model(): |
| if not model._meta.auto_created: |
| signals.post_delete.send( |
| sender=model, instance=obj, using=self.using |
| ) |
| |
| # update collected instances |
| for model, instances_for_fieldvalues in six.iteritems(self.field_updates): |
| for (field, value), instances in six.iteritems(instances_for_fieldvalues): |
| for obj in instances: |
| setattr(obj, field.attname, value) |
| for model, instances in six.iteritems(self.data): |
| for instance in instances: |
| setattr(instance, model._meta.pk.attname, None) |