Fixed #16891 -- Made Model/QuerySet.delete() return the number of deleted objects.
This commit is contained in:
parent
9c8a2ab81d
commit
04e8d890ae
|
@ -833,7 +833,7 @@ class Model(six.with_metaclass(ModelBase)):
|
|||
|
||||
collector = Collector(using=using)
|
||||
collector.collect([self], keep_parents=keep_parents)
|
||||
collector.delete()
|
||||
return collector.delete()
|
||||
|
||||
delete.alters_data = True
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from collections import OrderedDict
|
||||
from collections import Counter, OrderedDict
|
||||
from itertools import chain
|
||||
from operator import attrgetter
|
||||
|
||||
|
@ -280,6 +280,8 @@ class Collector(object):
|
|||
# don't support transactions or cannot defer constraint checks until the
|
||||
# end of a transaction.
|
||||
self.sort()
|
||||
# number of objects deleted for each model label
|
||||
deleted_counter = Counter()
|
||||
|
||||
with transaction.atomic(using=self.using, savepoint=False):
|
||||
# send pre_delete signals
|
||||
|
@ -291,7 +293,8 @@ class Collector(object):
|
|||
|
||||
# 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
|
||||
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):
|
||||
query = sql.DeleteQuery(model)
|
||||
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:
|
||||
for obj in instances:
|
||||
|
@ -324,3 +328,4 @@ class Collector(object):
|
|||
for model, instances in six.iteritems(self.data):
|
||||
for instance in instances:
|
||||
setattr(instance, model._meta.pk.attname, None)
|
||||
return sum(deleted_counter.values()), dict(deleted_counter)
|
||||
|
|
|
@ -590,10 +590,12 @@ class QuerySet(object):
|
|||
|
||||
collector = Collector(using=del_query.db)
|
||||
collector.collect(del_query)
|
||||
collector.delete()
|
||||
deleted, _rows_count = collector.delete()
|
||||
|
||||
# Clear the result cache, in case this QuerySet gets reused.
|
||||
self._result_cache = None
|
||||
return deleted, _rows_count
|
||||
|
||||
delete.alters_data = True
|
||||
delete.queryset_only = True
|
||||
|
||||
|
@ -602,7 +604,7 @@ class QuerySet(object):
|
|||
Deletes objects found from the given queryset in single direct SQL
|
||||
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
|
||||
|
||||
def update(self, **kwargs):
|
||||
|
|
|
@ -5,7 +5,9 @@ Query subclasses which provide extra functionality beyond simple data retrieval.
|
|||
from django.core.exceptions import FieldError
|
||||
from django.db import connections
|
||||
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.utils import six
|
||||
|
||||
|
@ -23,7 +25,8 @@ class DeleteQuery(Query):
|
|||
def do_query(self, table, where, using):
|
||||
self.tables = [table]
|
||||
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):
|
||||
"""
|
||||
|
@ -32,13 +35,16 @@ class DeleteQuery(Query):
|
|||
More than one physical query may be executed if there are a
|
||||
lot of values in pk_list.
|
||||
"""
|
||||
# number of objects deleted
|
||||
num_deleted = 0
|
||||
if not field:
|
||||
field = self.get_meta().pk
|
||||
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
|
||||
self.where = self.where_class()
|
||||
self.add_q(Q(
|
||||
**{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):
|
||||
"""
|
||||
|
@ -63,8 +69,7 @@ class DeleteQuery(Query):
|
|||
values = list(query.values_list('pk', flat=True))
|
||||
if not values:
|
||||
return
|
||||
self.delete_batch(values, using)
|
||||
return
|
||||
return self.delete_batch(values, using)
|
||||
else:
|
||||
innerq.clear_select_clause()
|
||||
innerq.select = [
|
||||
|
@ -73,7 +78,8 @@ class DeleteQuery(Query):
|
|||
values = innerq
|
||||
self.where = self.where_class()
|
||||
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):
|
||||
|
|
|
@ -537,7 +537,8 @@ Deleting objects
|
|||
|
||||
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
|
||||
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
|
||||
:ref:`topics-db-queries-delete`.
|
||||
|
@ -553,6 +554,10 @@ keep the parent model's data.
|
|||
|
||||
The ``keep_parents`` parameter was added.
|
||||
|
||||
.. versionchanged:: 1.9
|
||||
|
||||
The return value describing the number of objects deleted was added.
|
||||
|
||||
Pickling objects
|
||||
================
|
||||
|
||||
|
|
|
@ -2070,8 +2070,11 @@ delete
|
|||
|
||||
.. method:: delete()
|
||||
|
||||
Performs an SQL delete query on all rows in the :class:`.QuerySet`. The
|
||||
``delete()`` is applied instantly. You cannot call ``delete()`` on a
|
||||
Performs an SQL delete query on all rows in the :class:`.QuerySet` and
|
||||
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
|
||||
filtered.
|
||||
|
||||
|
@ -2081,15 +2084,22 @@ For example, to delete all the entries in a particular blog::
|
|||
|
||||
# Delete all the entries belonging to this Blog.
|
||||
>>> 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
|
||||
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.
|
||||
For example::
|
||||
|
||||
blogs = Blog.objects.all()
|
||||
>>> blogs = Blog.objects.all()
|
||||
|
||||
# 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
|
||||
:attr:`~django.db.models.ForeignKey.on_delete` argument to the
|
||||
|
|
|
@ -214,6 +214,10 @@ Models
|
|||
<django.db.models.Model.delete>` to allow deleting only a child's data in a
|
||||
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
|
||||
``order_with_respect_to`` on the same model.
|
||||
|
||||
|
|
|
@ -899,9 +899,15 @@ Deleting objects
|
|||
|
||||
The delete method, conveniently, is named
|
||||
: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
|
||||
: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
|
||||
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
|
||||
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
|
||||
: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
|
||||
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
|
||||
|
|
|
@ -137,6 +137,7 @@ class OnDeleteTests(TestCase):
|
|||
|
||||
|
||||
class DeletionTests(TestCase):
|
||||
|
||||
def test_m2m(self):
|
||||
m = M.objects.create()
|
||||
r = R.objects.create()
|
||||
|
@ -356,6 +357,62 @@ class DeletionTests(TestCase):
|
|||
self.assertFalse(RChild.objects.filter(id=child.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):
|
||||
|
||||
|
|
Loading…
Reference in New Issue