Ticket #1219: bulk-delete.patch
File bulk-delete.patch, 19.4 KB (added by , 19 years ago) |
---|
-
django/db/models/base.py
3 3 from django.db.models.fields import AutoField, ImageField 4 4 from django.db.models.fields.related import OneToOne, ManyToOne 5 5 from django.db.models.related import RelatedObject 6 from django.db.models.query import orderlist2sql 6 from django.db.models.query import orderlist2sql, delete_objects 7 7 from django.db.models.options import Options, AdminOptions 8 8 from django.db import connection, backend 9 9 from django.db.models import signals … … 49 49 register_models(new_class._meta.app_label, new_class) 50 50 return new_class 51 51 52 def cmp_cls(x, y):53 for field in x._meta.fields:54 if field.rel and not field.null and field.rel.to == y:55 return -156 for field in y._meta.fields:57 if field.rel and not field.null and field.rel.to == x:58 return 159 return 060 61 52 class Model(object): 62 53 __metaclass__ = ModelBase 63 54 … … 187 178 188 179 save.alters_data = True 189 180 190 def _ _collect_sub_objects(self, seen_objs):181 def _collect_sub_objects(self, seen_objs): 191 182 """ 192 183 Recursively populates seen_objs with all objects related to this object. 193 184 When done, seen_objs will be in the format: … … 207 198 except ObjectDoesNotExist: 208 199 pass 209 200 else: 210 sub_obj._ _collect_sub_objects(seen_objs)201 sub_obj._collect_sub_objects(seen_objs) 211 202 else: 212 203 for sub_obj in getattr(self, rel_opts_name).all(): 213 sub_obj._ _collect_sub_objects(seen_objs)204 sub_obj._collect_sub_objects(seen_objs) 214 205 215 206 def delete(self): 216 207 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) 208 209 # Find all the objects than need to be deleted 217 210 seen_objs = {} 218 self.__collect_sub_objects(seen_objs) 219 220 seen_classes = set(seen_objs.keys()) 221 ordered_classes = list(seen_classes) 222 ordered_classes.sort(cmp_cls) 223 224 cursor = connection.cursor() 225 226 for cls in ordered_classes: 227 seen_objs[cls] = seen_objs[cls].items() 228 seen_objs[cls].sort() 229 for pk_val, instance in seen_objs[cls]: 230 dispatcher.send(signal=signals.pre_delete, sender=cls, instance=instance) 231 232 for related in cls._meta.get_all_related_many_to_many_objects(): 233 cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ 234 (backend.quote_name(related.field.get_m2m_db_table(related.opts)), 235 backend.quote_name(cls._meta.object_name.lower() + '_id')), 236 [pk_val]) 237 for f in cls._meta.many_to_many: 238 cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ 239 (backend.quote_name(f.get_m2m_db_table(cls._meta)), 240 backend.quote_name(cls._meta.object_name.lower() + '_id')), 241 [pk_val]) 242 for field in cls._meta.fields: 243 if field.rel and field.null and field.rel.to in seen_classes: 244 cursor.execute("UPDATE %s SET %s=NULL WHERE %s=%%s" % \ 245 (backend.quote_name(cls._meta.db_table), backend.quote_name(field.column), 246 backend.quote_name(cls._meta.pk.column)), [pk_val]) 247 setattr(instance, field.attname, None) 248 249 for cls in ordered_classes: 250 seen_objs[cls].reverse() 251 for pk_val, instance in seen_objs[cls]: 252 cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ 253 (backend.quote_name(cls._meta.db_table), backend.quote_name(cls._meta.pk.column)), 254 [pk_val]) 255 setattr(instance, cls._meta.pk.attname, None) 256 dispatcher.send(signal=signals.post_delete, sender=cls, instance=instance) 257 258 connection.commit() 259 211 self._collect_sub_objects(seen_objs) 212 213 # Actually delete the objects 214 delete_objects(seen_objs) 215 260 216 delete.alters_data = True 261 217 262 218 def _get_FIELD_display(self, field): -
django/db/models/manager.py
57 57 def dates(self, *args, **kwargs): 58 58 return self.get_query_set().dates(*args, **kwargs) 59 59 60 def delete(self, *args, **kwargs):61 return self.get_query_set().delete(*args, **kwargs)62 63 60 def distinct(self, *args, **kwargs): 64 61 return self.get_query_set().distinct(*args, **kwargs) 65 62 -
django/db/models/query.py
1 1 from django.db import backend, connection 2 2 from django.db.models.fields import DateField, FieldDoesNotExist 3 from django.db.models import signals 4 from django.dispatch import dispatcher 3 5 from django.utils.datastructures import SortedDict 6 4 7 import operator 5 8 6 9 LOOKUP_SEPARATOR = '__' … … 125 128 extra_select = self._select.items() 126 129 127 130 cursor = connection.cursor() 128 select, sql, params = self._get_sql_clause( True)131 select, sql, params = self._get_sql_clause() 129 132 cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) 130 133 fill_cache = self._select_related 131 134 index_end = len(self.model._meta.fields) … … 149 152 counter._offset = None 150 153 counter._limit = None 151 154 counter._select_related = False 152 select, sql, params = counter._get_sql_clause( True)155 select, sql, params = counter._get_sql_clause() 153 156 cursor = connection.cursor() 154 157 cursor.execute("SELECT COUNT(*)" + sql, params) 155 158 return cursor.fetchone()[0] … … 171 174 assert bool(latest_by), "latest() requires either a field_name parameter or 'get_latest_by' in the model" 172 175 return self._clone(_limit=1, _order_by=('-'+latest_by,)).get() 173 176 174 def delete(self , *args, **kwargs):177 def delete(self): 175 178 """ 176 Deletes the records with the given kwargs. If no kwargs are given, 177 deletes records in the current QuerySet. 179 Deletes the records in the current QuerySet. 178 180 """ 179 # Remove the DELETE_ALL argument, if it exists. 180 delete_all = kwargs.pop('DELETE_ALL', False) 181 del_query = self._clone() 181 182 182 # Check for at least one query argument.183 if not kwargs and not delete_all:184 raise TypeError, "SAFETY MECHANISM: Specify DELETE_ALL=True if you actually want to delete all data."185 186 if kwargs:187 del_query = self.filter(*args, **kwargs)188 else:189 del_query = self._clone()190 183 # disable non-supported fields 191 184 del_query._select_related = False 192 del_query._select = {}193 185 del_query._order_by = [] 194 186 del_query._offset = None 195 187 del_query._limit = None 196 188 197 # Perform the SQL delete 198 cursor = connection.cursor() 199 _, sql, params = del_query._get_sql_clause(False) 200 cursor.execute("DELETE " + sql, params) 201 189 # Collect all the objects to be deleted, and all the objects that are related to 190 # the objects that are to be deleted 191 seen_objs = {} 192 for object in del_query: 193 object._collect_sub_objects(seen_objs) 194 195 # Delete the objects 196 delete_objects(seen_objs) 197 202 198 ################################################## 203 199 # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS # 204 200 ################################################## … … 297 293 self._result_cache = list(self.iterator()) 298 294 return self._result_cache 299 295 300 def _get_sql_clause(self , allow_joins):296 def _get_sql_clause(self): 301 297 opts = self.model._meta 302 298 303 299 # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z. … … 325 321 # Start composing the body of the SQL statement. 326 322 sql = [" FROM", backend.quote_name(opts.db_table)] 327 323 328 # Check if extra tables are allowed. If not, throw an error329 if (tables or joins) and not allow_joins:330 raise TypeError, "Joins are not allowed in this type of query"331 332 324 # Compose the join dictionary into SQL describing the joins. 333 325 if joins: 334 326 sql.append(" ".join(["%s %s AS %s ON %s" % (join_type, table, alias, condition) … … 407 399 field_names = [f.attname for f in self.model._meta.fields] 408 400 409 401 cursor = connection.cursor() 410 select, sql, params = self._get_sql_clause( True)402 select, sql, params = self._get_sql_clause() 411 403 select = ['%s.%s' % (backend.quote_name(self.model._meta.db_table), backend.quote_name(c)) for c in columns] 412 404 cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) 413 405 while 1: … … 429 421 if self._field.null: 430 422 date_query._where.append('%s.%s IS NOT NULL' % \ 431 423 (backend.quote_name(self.model._meta.db_table), backend.quote_name(self._field.column))) 432 select, sql, params = self._get_sql_clause( True)424 select, sql, params = self._get_sql_clause() 433 425 sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1 %s' % \ 434 426 (backend.get_date_trunc_sql(self._kind, '%s.%s' % (backend.quote_name(self.model._meta.db_table), 435 427 backend.quote_name(self._field.column))), sql, self._order) … … 762 754 params.extend(field.get_db_prep_lookup(clause, value)) 763 755 764 756 return tables, joins, where, params 757 758 def compare_models(x, y): 759 "Comparator for Models that puts models in an order where dependencies are easily resolved." 760 for field in x._meta.fields: 761 if field.rel and not field.null and field.rel.to == y: 762 return -1 763 for field in y._meta.fields: 764 if field.rel and not field.null and field.rel.to == x: 765 return 1 766 return 0 767 768 def delete_objects(seen_objs): 769 "Iterate through a list of seen classes, and remove any instances that are referred to" 770 seen_classes = set(seen_objs.keys()) 771 ordered_classes = list(seen_classes) 772 ordered_classes.sort(compare_models) 773 774 cursor = connection.cursor() 775 776 for cls in ordered_classes: 777 seen_objs[cls] = seen_objs[cls].items() 778 seen_objs[cls].sort() 779 780 # Pre notify all instances to be deleted 781 for pk_val, instance in seen_objs[cls]: 782 dispatcher.send(signal=signals.pre_delete, sender=cls, instance=instance) 783 784 pk_list = [pk for pk,instance in seen_objs[cls]] 785 for related in cls._meta.get_all_related_many_to_many_objects(): 786 cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ 787 (backend.quote_name(related.field.get_m2m_db_table(related.opts)), 788 backend.quote_name(cls._meta.object_name.lower() + '_id'), 789 ','.join('%s' for pk in pk_list)), 790 pk_list) 791 for f in cls._meta.many_to_many: 792 cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ 793 (backend.quote_name(f.get_m2m_db_table(cls._meta)), 794 backend.quote_name(cls._meta.object_name.lower() + '_id'), 795 ','.join(['%s' for pk in pk_list])), 796 pk_list) 797 for field in cls._meta.fields: 798 if field.rel and field.null and field.rel.to in seen_classes: 799 cursor.execute("UPDATE %s SET %s=NULL WHERE %s IN (%s)" % \ 800 (backend.quote_name(cls._meta.db_table), 801 backend.quote_name(field.column), 802 backend.quote_name(cls._meta.pk.column), 803 ','.join(['%s' for pk in pk_list])), 804 pk_list) 805 806 # Now delete the actual data 807 for cls in ordered_classes: 808 seen_objs[cls].reverse() 809 pk_list = [pk for pk,instance in seen_objs[cls]] 810 811 cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ 812 (backend.quote_name(cls._meta.db_table), 813 backend.quote_name(cls._meta.pk.column), 814 ','.join(['%s' for pk in pk_list])), 815 pk_list) 816 817 # Last cleanup; set NULLs where there once was a reference to the object, 818 # NULL the primary key of the found objects, and perform post-notification. 819 for pk_val, instance in seen_objs[cls]: 820 for field in cls._meta.fields: 821 if field.rel and field.null and field.rel.to in seen_classes: 822 setattr(instance, field.attname, None) 823 824 setattr(instance, cls._meta.pk.attname, None) 825 dispatcher.send(signal=signals.post_delete, sender=cls, instance=instance) 826 827 connection.commit() -
tests/modeltests/basic/models.py
9 9 class Article(models.Model): 10 10 headline = models.CharField(maxlength=100, default='Default headline') 11 11 pub_date = models.DateTimeField() 12 12 13 def __repr__(self): 14 return self.headline 13 15 API_TESTS = """ 14 16 15 17 # No articles are in the system yet. … … 41 43 # the article is represented by "<Article object>", because we haven't given 42 44 # the Article model a __repr__() method. 43 45 >>> Article.objects.all() 44 [ <Article object>]46 [Area woman programs in Python] 45 47 46 48 # Django provides a rich database lookup API. 47 49 >>> Article.objects.get(id__exact=1) 48 <Article object> 50 Area woman programs in Python 49 51 >>> Article.objects.get(headline__startswith='Area woman') 50 <Article object> 52 Area woman programs in Python 51 53 >>> Article.objects.get(pub_date__year=2005) 52 <Article object> 54 Area woman programs in Python 53 55 >>> Article.objects.get(pub_date__year=2005, pub_date__month=7) 54 <Article object> 56 Area woman programs in Python 55 57 >>> Article.objects.get(pub_date__year=2005, pub_date__month=7, pub_date__day=28) 56 <Article object> 58 Area woman programs in Python 57 59 58 60 # The "__exact" lookup type can be omitted, as a shortcut. 59 61 >>> Article.objects.get(id=1) 60 <Article object> 62 Area woman programs in Python 61 63 >>> Article.objects.get(headline='Area woman programs in Python') 62 <Article object> 64 Area woman programs in Python 63 65 64 66 >>> Article.objects.filter(pub_date__year=2005) 65 [ <Article object>]67 [Area woman programs in Python] 66 68 >>> Article.objects.filter(pub_date__year=2004) 67 69 [] 68 70 >>> Article.objects.filter(pub_date__year=2005, pub_date__month=7) 69 [ <Article object>]71 [Area woman programs in Python] 70 72 71 73 # Django raises an Article.DoesNotExist exception for get() if the parameters 72 74 # don't match any object. … … 84 86 # shortcut for primary-key exact lookups. 85 87 # The following is identical to articles.get(id=1). 86 88 >>> Article.objects.get(pk=1) 87 <Article object> 89 Area woman programs in Python 88 90 89 91 # Model instances of the same type and same ID are considered equal. 90 92 >>> a = Article.objects.get(pk=1) … … 234 236 235 237 # You can get items using index and slice notation. 236 238 >>> Article.objects.all()[0] 237 <Article object> 239 Area woman programs in Python 238 240 >>> Article.objects.all()[1:3] 239 [ <Article object>, <Article object>]241 [Second article, Third article] 240 242 >>> s3 = Article.objects.filter(id__exact=3) 241 243 >>> (s1 | s2 | s3)[::2] 242 [ <Article object>, <Article object>]244 [Area woman programs in Python, Third article] 243 245 244 246 # An Article instance doesn't have access to the "objects" attribute. 245 247 # That's only available on the class. … … 254 256 AttributeError: Manager isn't accessible via Article instances 255 257 256 258 # Bulk delete test: How many objects before and after the delete? 257 >>> Article.objects. count()258 8L 259 >>> Article.objects. delete(id__lte=4)260 >>> Article.objects. count()261 4L 259 >>> Article.objects.all() 260 [Area woman programs in Python, Second article, Third article, Fourth article, Article 6, Default headline, Article 7, Updated article 8] 261 >>> Article.objects.filter(id__lte=4).delete() 262 >>> Article.objects.all() 263 [Article 6, Default headline, Article 7, Updated article 8] 262 264 263 >>> Article.objects.delete()264 Traceback (most recent call last):265 ...266 TypeError: SAFETY MECHANISM: Specify DELETE_ALL=True if you actually want to delete all data.267 268 >>> Article.objects.delete(DELETE_ALL=True)269 >>> Article.objects.count()270 0L271 272 265 """ 273 266 274 267 from django.conf import settings -
tests/modeltests/many_to_many/models.py
162 162 >>> p2.article_set.all().order_by('headline') 163 163 [Oxygen-free diet works wonders] 164 164 165 # Recreate the article and Publication we just deleted. 166 >>> p1 = Publication(id=None, title='The Python Journal') 167 >>> p1.save() 168 >>> a2 = Article(id=None, headline='NASA uses Python') 169 >>> a2.save() 170 >>> a2.publications.add(p1, p2, p3) 165 171 172 # Bulk delete some Publications - references to deleted publications should go 173 >>> Publication.objects.filter(title__startswith='Science').delete() 174 >>> Publication.objects.all() 175 [Highlights for Children, The Python Journal] 176 >>> Article.objects.all() 177 [Django lets you build Web apps easily, NASA finds intelligent life on Earth, Oxygen-free diet works wonders, NASA uses Python] 178 >>> a2.publications.all() 179 [The Python Journal] 166 180 181 # Bulk delete some articles - references to deleted objects should go 182 >>> Article.objects.filter(headline__startswith='Django').delete() 183 >>> p1.article_set.all() 184 [NASA uses Python] 185 186 167 187 """ -
tests/modeltests/many_to_one/models.py
94 94 95 95 # The underlying query only makes one join when a related table is referenced twice. 96 96 >>> query = Article.objects.filter(reporter__first_name__exact='John', reporter__last_name__exact='Smith') 97 >>> null, sql, null = query._get_sql_clause( True)97 >>> null, sql, null = query._get_sql_clause() 98 98 >>> sql.count('INNER JOIN') 99 99 1 100 100 … … 163 163 >>> Reporter.objects.filter(article__reporter__first_name__startswith='John').distinct() 164 164 [John Smith] 165 165 166 # Deletes that require joins are prohibited.167 >>> Article.objects.delete(reporter__first_name__startswith='Jo')168 Traceback (most recent call last):169 ...170 TypeError: Joins are not allowed in this type of query171 172 166 # If you delete a reporter, his articles will be deleted. 173 167 >>> Article.objects.order_by('headline') 174 168 [John's second story, Paul's story, This is a test, This is a test, This is a test] 175 169 >>> Reporter.objects.order_by('first_name') 176 170 [John Smith, Paul Jones] 177 >>> r .delete()171 >>> r2.delete() 178 172 >>> Article.objects.order_by('headline') 179 [ Paul's story]173 [John's second story, This is a test, This is a test, This is a test] 180 174 >>> Reporter.objects.order_by('first_name') 181 [ Paul Jones]175 [John Smith] 182 176 177 # Deletes using a join in the query 178 >>> Reporter.objects.filter(article__headline__startswith='This').delete() 179 >>> Reporter.objects.all() 180 [] 181 >>> Article.objects.all() 182 [] 183 183 184 """