Fixed #16891 -- Made Model/QuerySet.delete() return the number of deleted objects.

This commit is contained in:
Alexander Sosnovskiy 2015-03-07 11:56:25 +03:00 committed by Tim Graham
parent 9c8a2ab81d
commit 04e8d890ae
9 changed files with 120 additions and 20 deletions

View File

@ -833,7 +833,7 @@ class Model(six.with_metaclass(ModelBase)):
collector = Collector(using=using) collector = Collector(using=using)
collector.collect([self], keep_parents=keep_parents) collector.collect([self], keep_parents=keep_parents)
collector.delete() return collector.delete()
delete.alters_data = True delete.alters_data = True

View File

@ -1,4 +1,4 @@
from collections import OrderedDict from collections import Counter, OrderedDict
from itertools import chain from itertools import chain
from operator import attrgetter from operator import attrgetter
@ -280,6 +280,8 @@ class Collector(object):
# don't support transactions or cannot defer constraint checks until the # don't support transactions or cannot defer constraint checks until the
# end of a transaction. # end of a transaction.
self.sort() self.sort()
# number of objects deleted for each model label
deleted_counter = Counter()
with transaction.atomic(using=self.using, savepoint=False): with transaction.atomic(using=self.using, savepoint=False):
# send pre_delete signals # send pre_delete signals
@ -291,7 +293,8 @@ class Collector(object):
# fast deletes # fast deletes
for qs in self.fast_deletes: for qs in self.fast_deletes:
qs._raw_delete(using=self.using) count = qs._raw_delete(using=self.using)
deleted_counter[qs.model._meta.label] += count
# update fields # update fields
for model, instances_for_fieldvalues in six.iteritems(self.field_updates): for model, instances_for_fieldvalues in six.iteritems(self.field_updates):
@ -308,7 +311,8 @@ class Collector(object):
for model, instances in six.iteritems(self.data): for model, instances in six.iteritems(self.data):
query = sql.DeleteQuery(model) query = sql.DeleteQuery(model)
pk_list = [obj.pk for obj in instances] pk_list = [obj.pk for obj in instances]
query.delete_batch(pk_list, self.using) count = query.delete_batch(pk_list, self.using)
deleted_counter[model._meta.label] += count
if not model._meta.auto_created: if not model._meta.auto_created:
for obj in instances: for obj in instances:
@ -324,3 +328,4 @@ class Collector(object):
for model, instances in six.iteritems(self.data): for model, instances in six.iteritems(self.data):
for instance in instances: for instance in instances:
setattr(instance, model._meta.pk.attname, None) setattr(instance, model._meta.pk.attname, None)
return sum(deleted_counter.values()), dict(deleted_counter)

View File

@ -590,10 +590,12 @@ class QuerySet(object):
collector = Collector(using=del_query.db) collector = Collector(using=del_query.db)
collector.collect(del_query) collector.collect(del_query)
collector.delete() deleted, _rows_count = collector.delete()
# Clear the result cache, in case this QuerySet gets reused. # Clear the result cache, in case this QuerySet gets reused.
self._result_cache = None self._result_cache = None
return deleted, _rows_count
delete.alters_data = True delete.alters_data = True
delete.queryset_only = True delete.queryset_only = True
@ -602,7 +604,7 @@ class QuerySet(object):
Deletes objects found from the given queryset in single direct SQL Deletes objects found from the given queryset in single direct SQL
query. No signals are sent, and there is no protection for cascades. query. No signals are sent, and there is no protection for cascades.
""" """
sql.DeleteQuery(self.model).delete_qs(self, using) return sql.DeleteQuery(self.model).delete_qs(self, using)
_raw_delete.alters_data = True _raw_delete.alters_data = True
def update(self, **kwargs): def update(self, **kwargs):

View File

@ -5,7 +5,9 @@ Query subclasses which provide extra functionality beyond simple data retrieval.
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.db import connections from django.db import connections
from django.db.models.query_utils import Q from django.db.models.query_utils import Q
from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, NO_RESULTS from django.db.models.sql.constants import (
CURSOR, GET_ITERATOR_CHUNK_SIZE, NO_RESULTS,
)
from django.db.models.sql.query import Query from django.db.models.sql.query import Query
from django.utils import six from django.utils import six
@ -23,7 +25,8 @@ class DeleteQuery(Query):
def do_query(self, table, where, using): def do_query(self, table, where, using):
self.tables = [table] self.tables = [table]
self.where = where self.where = where
self.get_compiler(using).execute_sql(NO_RESULTS) cursor = self.get_compiler(using).execute_sql(CURSOR)
return cursor.rowcount if cursor else 0
def delete_batch(self, pk_list, using, field=None): def delete_batch(self, pk_list, using, field=None):
""" """
@ -32,13 +35,16 @@ class DeleteQuery(Query):
More than one physical query may be executed if there are a More than one physical query may be executed if there are a
lot of values in pk_list. lot of values in pk_list.
""" """
# number of objects deleted
num_deleted = 0
if not field: if not field:
field = self.get_meta().pk field = self.get_meta().pk
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
self.where = self.where_class() self.where = self.where_class()
self.add_q(Q( self.add_q(Q(
**{field.attname + '__in': pk_list[offset:offset + GET_ITERATOR_CHUNK_SIZE]})) **{field.attname + '__in': pk_list[offset:offset + GET_ITERATOR_CHUNK_SIZE]}))
self.do_query(self.get_meta().db_table, self.where, using=using) num_deleted += self.do_query(self.get_meta().db_table, self.where, using=using)
return num_deleted
def delete_qs(self, query, using): def delete_qs(self, query, using):
""" """
@ -63,8 +69,7 @@ class DeleteQuery(Query):
values = list(query.values_list('pk', flat=True)) values = list(query.values_list('pk', flat=True))
if not values: if not values:
return return
self.delete_batch(values, using) return self.delete_batch(values, using)
return
else: else:
innerq.clear_select_clause() innerq.clear_select_clause()
innerq.select = [ innerq.select = [
@ -73,7 +78,8 @@ class DeleteQuery(Query):
values = innerq values = innerq
self.where = self.where_class() self.where = self.where_class()
self.add_q(Q(pk__in=values)) self.add_q(Q(pk__in=values))
self.get_compiler(using).execute_sql(NO_RESULTS) cursor = self.get_compiler(using).execute_sql(CURSOR)
return cursor.rowcount if cursor else 0
class UpdateQuery(Query): class UpdateQuery(Query):

View File

@ -537,7 +537,8 @@ Deleting objects
Issues an SQL ``DELETE`` for the object. This only deletes the object in the Issues an SQL ``DELETE`` for the object. This only deletes the object in the
database; the Python instance will still exist and will still have data in database; the Python instance will still exist and will still have data in
its fields. its fields. This method returns the number of objects deleted and a dictionary
with the number of deletions per object type.
For more details, including how to delete objects in bulk, see For more details, including how to delete objects in bulk, see
:ref:`topics-db-queries-delete`. :ref:`topics-db-queries-delete`.
@ -553,6 +554,10 @@ keep the parent model's data.
The ``keep_parents`` parameter was added. The ``keep_parents`` parameter was added.
.. versionchanged:: 1.9
The return value describing the number of objects deleted was added.
Pickling objects Pickling objects
================ ================

View File

@ -2070,8 +2070,11 @@ delete
.. method:: delete() .. method:: delete()
Performs an SQL delete query on all rows in the :class:`.QuerySet`. The Performs an SQL delete query on all rows in the :class:`.QuerySet` and
``delete()`` is applied instantly. You cannot call ``delete()`` on a returns the number of objects deleted and a dictionary with the number of
deletions per object type.
The ``delete()`` is applied instantly. You cannot call ``delete()`` on a
:class:`.QuerySet` that has had a slice taken or can otherwise no longer be :class:`.QuerySet` that has had a slice taken or can otherwise no longer be
filtered. filtered.
@ -2081,15 +2084,22 @@ For example, to delete all the entries in a particular blog::
# Delete all the entries belonging to this Blog. # Delete all the entries belonging to this Blog.
>>> Entry.objects.filter(blog=b).delete() >>> Entry.objects.filter(blog=b).delete()
(4, {'weblog.Entry': 2, 'weblog.Entry_authors': 2})
.. versionchanged:: 1.9
The return value describing the number of objects deleted was added.
By default, Django's :class:`~django.db.models.ForeignKey` emulates the SQL By default, Django's :class:`~django.db.models.ForeignKey` emulates the SQL
constraint ``ON DELETE CASCADE`` — in other words, any objects with foreign constraint ``ON DELETE CASCADE`` — in other words, any objects with foreign
keys pointing at the objects to be deleted will be deleted along with them. keys pointing at the objects to be deleted will be deleted along with them.
For example:: For example::
blogs = Blog.objects.all() >>> blogs = Blog.objects.all()
# This will delete all Blogs and all of their Entry objects. # This will delete all Blogs and all of their Entry objects.
blogs.delete() >>> blogs.delete()
(5, {'weblog.Blog': 1, 'weblog.Entry': 2, 'weblog.Entry_authors': 2})
This cascade behavior is customizable via the This cascade behavior is customizable via the
:attr:`~django.db.models.ForeignKey.on_delete` argument to the :attr:`~django.db.models.ForeignKey.on_delete` argument to the

View File

@ -214,6 +214,10 @@ Models
<django.db.models.Model.delete>` to allow deleting only a child's data in a <django.db.models.Model.delete>` to allow deleting only a child's data in a
model that uses multi-table inheritance. model that uses multi-table inheritance.
* :meth:`Model.delete() <django.db.models.Model.delete>`
and :meth:`QuerySet.delete() <django.db.models.query.QuerySet.delete>` return
the number of objects deleted.
* Added a system check to prevent defining both ``Meta.ordering`` and * Added a system check to prevent defining both ``Meta.ordering`` and
``order_with_respect_to`` on the same model. ``order_with_respect_to`` on the same model.

View File

@ -899,9 +899,15 @@ Deleting objects
The delete method, conveniently, is named The delete method, conveniently, is named
:meth:`~django.db.models.Model.delete`. This method immediately deletes the :meth:`~django.db.models.Model.delete`. This method immediately deletes the
object and has no return value. Example:: object and returns the number of objects deleted and a dictionary with
the number of deletions per object type. Example::
e.delete() >>> e.delete()
(1, {'weblog.Entry': 1})
.. versionchanged:: 1.9
The return value describing the number of objects deleted was added.
You can also delete objects in bulk. Every You can also delete objects in bulk. Every
:class:`~django.db.models.query.QuerySet` has a :class:`~django.db.models.query.QuerySet` has a
@ -911,7 +917,8 @@ members of that :class:`~django.db.models.query.QuerySet`.
For example, this deletes all ``Entry`` objects with a ``pub_date`` year of For example, this deletes all ``Entry`` objects with a ``pub_date`` year of
2005:: 2005::
Entry.objects.filter(pub_date__year=2005).delete() >>> Entry.objects.filter(pub_date__year=2005).delete()
(5, {'webapp.Entry': 5})
Keep in mind that this will, whenever possible, be executed purely in SQL, and Keep in mind that this will, whenever possible, be executed purely in SQL, and
so the ``delete()`` methods of individual object instances will not necessarily so the ``delete()`` methods of individual object instances will not necessarily
@ -923,6 +930,10 @@ object individually) rather than using the bulk
:meth:`~django.db.models.query.QuerySet.delete` method of a :meth:`~django.db.models.query.QuerySet.delete` method of a
:class:`~django.db.models.query.QuerySet`. :class:`~django.db.models.query.QuerySet`.
.. versionchanged:: 1.9
The return value describing the number of objects deleted was added.
When Django deletes an object, by default it emulates the behavior of the SQL When Django deletes an object, by default it emulates the behavior of the SQL
constraint ``ON DELETE CASCADE`` -- in other words, any objects which had constraint ``ON DELETE CASCADE`` -- in other words, any objects which had
foreign keys pointing at the object to be deleted will be deleted along with foreign keys pointing at the object to be deleted will be deleted along with

View File

@ -137,6 +137,7 @@ class OnDeleteTests(TestCase):
class DeletionTests(TestCase): class DeletionTests(TestCase):
def test_m2m(self): def test_m2m(self):
m = M.objects.create() m = M.objects.create()
r = R.objects.create() r = R.objects.create()
@ -356,6 +357,62 @@ class DeletionTests(TestCase):
self.assertFalse(RChild.objects.filter(id=child.id).exists()) self.assertFalse(RChild.objects.filter(id=child.id).exists())
self.assertTrue(R.objects.filter(id=parent_id).exists()) self.assertTrue(R.objects.filter(id=parent_id).exists())
def test_queryset_delete_returns_num_rows(self):
"""
QuerySet.delete() should return the number of deleted rows and a
dictionary with the number of deletions for each object type.
"""
Avatar.objects.bulk_create([Avatar(desc='a'), Avatar(desc='b'), Avatar(desc='c')])
avatars_count = Avatar.objects.count()
deleted, rows_count = Avatar.objects.all().delete()
self.assertEqual(deleted, avatars_count)
# more complex example with multiple object types
r = R.objects.create()
h1 = HiddenUser.objects.create(r=r)
HiddenUser.objects.create(r=r)
HiddenUserProfile.objects.create(user=h1)
existed_objs = {
R._meta.label: R.objects.count(),
HiddenUser._meta.label: HiddenUser.objects.count(),
A._meta.label: A.objects.count(),
MR._meta.label: MR.objects.count(),
HiddenUserProfile._meta.label: HiddenUserProfile.objects.count(),
}
deleted, deleted_objs = R.objects.all().delete()
for k, v in existed_objs.items():
self.assertEqual(deleted_objs[k], v)
def test_model_delete_returns_num_rows(self):
"""
Model.delete() should return the number of deleted rows and a
dictionary with the number of deletions for each object type.
"""
r = R.objects.create()
h1 = HiddenUser.objects.create(r=r)
h2 = HiddenUser.objects.create(r=r)
HiddenUser.objects.create(r=r)
HiddenUserProfile.objects.create(user=h1)
HiddenUserProfile.objects.create(user=h2)
m1 = M.objects.create()
m2 = M.objects.create()
MR.objects.create(r=r, m=m1)
r.m_set.add(m1)
r.m_set.add(m2)
r.save()
existed_objs = {
R._meta.label: R.objects.count(),
HiddenUser._meta.label: HiddenUser.objects.count(),
A._meta.label: A.objects.count(),
MR._meta.label: MR.objects.count(),
HiddenUserProfile._meta.label: HiddenUserProfile.objects.count(),
M.m2m.through._meta.label: M.m2m.through.objects.count(),
}
deleted, deleted_objs = r.delete()
self.assertEqual(deleted, sum(existed_objs.values()))
for k, v in existed_objs.items():
self.assertEqual(deleted_objs[k], v)
class FastDeleteTests(TestCase): class FastDeleteTests(TestCase):