Permalink
Browse files

magic-removal: Fixed #1219 -- Added implementation of bulk delete, an…

…d factored common deletion code out of individual models.

git-svn-id: http://code.djangoproject.com/svn/django/branches/magic-removal@2307 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent c9839b8 commit a1d9de4dc78e476d7f616c78f0347f501169d9df @freakboy3742 freakboy3742 committed Feb 14, 2006
View
@@ -3,7 +3,7 @@
from django.db.models.fields import AutoField, ImageField
from django.db.models.fields.related import OneToOne, ManyToOne
from django.db.models.related import RelatedObject
-from django.db.models.query import orderlist2sql
+from django.db.models.query import orderlist2sql, delete_objects
from django.db.models.options import Options, AdminOptions
from django.db import connection, backend
from django.db.models import signals
@@ -49,15 +49,6 @@ def __new__(cls, name, bases, attrs):
register_models(new_class._meta.app_label, new_class)
return new_class
-def cmp_cls(x, y):
- for field in x._meta.fields:
- if field.rel and not field.null and field.rel.to == y:
- return -1
- for field in y._meta.fields:
- if field.rel and not field.null and field.rel.to == x:
- return 1
- return 0
-
class Model(object):
__metaclass__ = ModelBase
@@ -187,7 +178,7 @@ def save(self):
save.alters_data = True
- def __collect_sub_objects(self, seen_objs):
+ def _collect_sub_objects(self, seen_objs):
"""
Recursively populates seen_objs with all objects related to this object.
When done, seen_objs will be in the format:
@@ -207,56 +198,21 @@ def __collect_sub_objects(self, seen_objs):
except ObjectDoesNotExist:
pass
else:
- sub_obj.__collect_sub_objects(seen_objs)
+ sub_obj._collect_sub_objects(seen_objs)
else:
for sub_obj in getattr(self, rel_opts_name).all():
- sub_obj.__collect_sub_objects(seen_objs)
+ sub_obj._collect_sub_objects(seen_objs)
def delete(self):
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)
+
+ # Find all the objects than need to be deleted
seen_objs = {}
- self.__collect_sub_objects(seen_objs)
-
- seen_classes = set(seen_objs.keys())
- ordered_classes = list(seen_classes)
- ordered_classes.sort(cmp_cls)
-
- cursor = connection.cursor()
-
- for cls in ordered_classes:
- seen_objs[cls] = seen_objs[cls].items()
- seen_objs[cls].sort()
- for pk_val, instance in seen_objs[cls]:
- dispatcher.send(signal=signals.pre_delete, sender=cls, instance=instance)
-
- for related in cls._meta.get_all_related_many_to_many_objects():
- cursor.execute("DELETE FROM %s WHERE %s=%%s" % \
- (backend.quote_name(related.field.get_m2m_db_table(related.opts)),
- backend.quote_name(cls._meta.object_name.lower() + '_id')),
- [pk_val])
- for f in cls._meta.many_to_many:
- cursor.execute("DELETE FROM %s WHERE %s=%%s" % \
- (backend.quote_name(f.get_m2m_db_table(cls._meta)),
- backend.quote_name(cls._meta.object_name.lower() + '_id')),
- [pk_val])
- for field in cls._meta.fields:
- if field.rel and field.null and field.rel.to in seen_classes:
- cursor.execute("UPDATE %s SET %s=NULL WHERE %s=%%s" % \
- (backend.quote_name(cls._meta.db_table), backend.quote_name(field.column),
- backend.quote_name(cls._meta.pk.column)), [pk_val])
- setattr(instance, field.attname, None)
-
- for cls in ordered_classes:
- seen_objs[cls].reverse()
- for pk_val, instance in seen_objs[cls]:
- cursor.execute("DELETE FROM %s WHERE %s=%%s" % \
- (backend.quote_name(cls._meta.db_table), backend.quote_name(cls._meta.pk.column)),
- [pk_val])
- setattr(instance, cls._meta.pk.attname, None)
- dispatcher.send(signal=signals.post_delete, sender=cls, instance=instance)
-
- connection.commit()
-
+ self._collect_sub_objects(seen_objs)
+
+ # Actually delete the objects
+ delete_objects(seen_objs)
+
delete.alters_data = True
def _get_FIELD_display(self, field):
@@ -57,9 +57,6 @@ def count(self):
def dates(self, *args, **kwargs):
return self.get_query_set().dates(*args, **kwargs)
- def delete(self, *args, **kwargs):
- return self.get_query_set().delete(*args, **kwargs)
-
def distinct(self, *args, **kwargs):
return self.get_query_set().distinct(*args, **kwargs)
View
@@ -1,6 +1,9 @@
from django.db import backend, connection
from django.db.models.fields import DateField, FieldDoesNotExist
+from django.db.models import signals
+from django.dispatch import dispatcher
from django.utils.datastructures import SortedDict
+
import operator
LOOKUP_SEPARATOR = '__'
@@ -125,7 +128,7 @@ def iterator(self):
extra_select = self._select.items()
cursor = connection.cursor()
- select, sql, params = self._get_sql_clause(True)
+ select, sql, params = self._get_sql_clause()
cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params)
fill_cache = self._select_related
index_end = len(self.model._meta.fields)
@@ -149,7 +152,7 @@ def count(self):
counter._offset = None
counter._limit = None
counter._select_related = False
- select, sql, params = counter._get_sql_clause(True)
+ select, sql, params = counter._get_sql_clause()
cursor = connection.cursor()
cursor.execute("SELECT COUNT(*)" + sql, params)
return cursor.fetchone()[0]
@@ -171,34 +174,31 @@ def latest(self, field_name=None):
assert bool(latest_by), "latest() requires either a field_name parameter or 'get_latest_by' in the model"
return self._clone(_limit=1, _order_by=('-'+latest_by,)).get()
- def delete(self, *args, **kwargs):
+ def delete(self):
"""
- Deletes the records with the given kwargs. If no kwargs are given,
- deletes records in the current QuerySet.
+ Deletes the records in the current QuerySet.
"""
- # Remove the DELETE_ALL argument, if it exists.
- delete_all = kwargs.pop('DELETE_ALL', False)
-
- # Check for at least one query argument.
- if not kwargs and not delete_all:
- raise TypeError, "SAFETY MECHANISM: Specify DELETE_ALL=True if you actually want to delete all data."
+ del_query = self._clone()
- if kwargs:
- del_query = self.filter(*args, **kwargs)
- else:
- del_query = self._clone()
# disable non-supported fields
del_query._select_related = False
- del_query._select = {}
del_query._order_by = []
del_query._offset = None
del_query._limit = None
- # Perform the SQL delete
- cursor = connection.cursor()
- _, sql, params = del_query._get_sql_clause(False)
- cursor.execute("DELETE " + sql, params)
-
+ # Collect all the objects to be deleted, and all the objects that are related to
+ # the objects that are to be deleted
+ seen_objs = {}
+ for object in del_query:
+ object._collect_sub_objects(seen_objs)
+
+ # Delete the objects
+ delete_objects(seen_objs)
+
+ # Clear the result cache, in case this QuerySet gets reused.
+ self._result_cache = None
+ delete.alters_data = True
+
##################################################
# PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS #
##################################################
@@ -297,7 +297,7 @@ def _get_data(self):
self._result_cache = list(self.iterator())
return self._result_cache
- def _get_sql_clause(self, allow_joins):
+ def _get_sql_clause(self):
opts = self.model._meta
# Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z.
@@ -325,10 +325,6 @@ def _get_sql_clause(self, allow_joins):
# Start composing the body of the SQL statement.
sql = [" FROM", backend.quote_name(opts.db_table)]
- # Check if extra tables are allowed. If not, throw an error
- if (tables or joins) and not allow_joins:
- raise TypeError, "Joins are not allowed in this type of query"
-
# Compose the join dictionary into SQL describing the joins.
if joins:
sql.append(" ".join(["%s %s AS %s ON %s" % (join_type, table, alias, condition)
@@ -407,7 +403,7 @@ def iterator(self):
field_names = [f.attname for f in self.model._meta.fields]
cursor = connection.cursor()
- select, sql, params = self._get_sql_clause(True)
+ select, sql, params = self._get_sql_clause()
select = ['%s.%s' % (backend.quote_name(self.model._meta.db_table), backend.quote_name(c)) for c in columns]
cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params)
while 1:
@@ -429,7 +425,7 @@ def iterator(self):
if self._field.null:
date_query._where.append('%s.%s IS NOT NULL' % \
(backend.quote_name(self.model._meta.db_table), backend.quote_name(self._field.column)))
- select, sql, params = self._get_sql_clause(True)
+ select, sql, params = self._get_sql_clause()
sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1 %s' % \
(backend.get_date_trunc_sql(self._kind, '%s.%s' % (backend.quote_name(self.model._meta.db_table),
backend.quote_name(self._field.column))), sql, self._order)
@@ -762,3 +758,74 @@ def lookup_inner(path, clause, value, opts, table, column):
params.extend(field.get_db_prep_lookup(clause, value))
return tables, joins, where, params
+
+def compare_models(x, y):
+ "Comparator for Models that puts models in an order where dependencies are easily resolved."
+ for field in x._meta.fields:
+ if field.rel and not field.null and field.rel.to == y:
+ return -1
+ for field in y._meta.fields:
+ if field.rel and not field.null and field.rel.to == x:
+ return 1
+ return 0
+
+def delete_objects(seen_objs):
+ "Iterate through a list of seen classes, and remove any instances that are referred to"
+ seen_classes = set(seen_objs.keys())
+ ordered_classes = list(seen_classes)
+ ordered_classes.sort(compare_models)
+
+ cursor = connection.cursor()
+
+ for cls in ordered_classes:
+ seen_objs[cls] = seen_objs[cls].items()
+ seen_objs[cls].sort()
+
+ # Pre notify all instances to be deleted
+ for pk_val, instance in seen_objs[cls]:
+ dispatcher.send(signal=signals.pre_delete, sender=cls, instance=instance)
+
+ pk_list = [pk for pk,instance in seen_objs[cls]]
+ for related in cls._meta.get_all_related_many_to_many_objects():
+ cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \
+ (backend.quote_name(related.field.get_m2m_db_table(related.opts)),
+ backend.quote_name(cls._meta.object_name.lower() + '_id'),
+ ','.join('%s' for pk in pk_list)),
+ pk_list)
+ for f in cls._meta.many_to_many:
+ cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \
+ (backend.quote_name(f.get_m2m_db_table(cls._meta)),
+ backend.quote_name(cls._meta.object_name.lower() + '_id'),
+ ','.join(['%s' for pk in pk_list])),
+ pk_list)
+ for field in cls._meta.fields:
+ if field.rel and field.null and field.rel.to in seen_classes:
+ cursor.execute("UPDATE %s SET %s=NULL WHERE %s IN (%s)" % \
+ (backend.quote_name(cls._meta.db_table),
+ backend.quote_name(field.column),
+ backend.quote_name(cls._meta.pk.column),
+ ','.join(['%s' for pk in pk_list])),
+ pk_list)
+
+ # Now delete the actual data
+ for cls in ordered_classes:
+ seen_objs[cls].reverse()
+ pk_list = [pk for pk,instance in seen_objs[cls]]
+
+ cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \
+ (backend.quote_name(cls._meta.db_table),
+ backend.quote_name(cls._meta.pk.column),
+ ','.join(['%s' for pk in pk_list])),
+ pk_list)
+
+ # Last cleanup; set NULLs where there once was a reference to the object,
+ # NULL the primary key of the found objects, and perform post-notification.
+ for pk_val, instance in seen_objs[cls]:
+ for field in cls._meta.fields:
+ if field.rel and field.null and field.rel.to in seen_classes:
+ setattr(instance, field.attname, None)
+
+ setattr(instance, cls._meta.pk.attname, None)
+ dispatcher.send(signal=signals.post_delete, sender=cls, instance=instance)
+
+ connection.commit()
Oops, something went wrong.

0 comments on commit a1d9de4

Please sign in to comment.