Ticket #1219: bulk-delete-v2.patch
File bulk-delete-v2.patch, 20.0 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 198 # Clear the result cache, in case this QuerySet gets reused. 199 self._result_cache = None 200 delete.alters_data = True 201 202 202 ################################################## 203 203 # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS # 204 204 ################################################## … … 297 297 self._result_cache = list(self.iterator()) 298 298 return self._result_cache 299 299 300 def _get_sql_clause(self , allow_joins):300 def _get_sql_clause(self): 301 301 opts = self.model._meta 302 302 303 303 # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z. … … 325 325 # Start composing the body of the SQL statement. 326 326 sql = [" FROM", backend.quote_name(opts.db_table)] 327 327 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 328 # Compose the join dictionary into SQL describing the joins. 333 329 if joins: 334 330 sql.append(" ".join(["%s %s AS %s ON %s" % (join_type, table, alias, condition) … … 407 403 field_names = [f.attname for f in self.model._meta.fields] 408 404 409 405 cursor = connection.cursor() 410 select, sql, params = self._get_sql_clause( True)406 select, sql, params = self._get_sql_clause() 411 407 select = ['%s.%s' % (backend.quote_name(self.model._meta.db_table), backend.quote_name(c)) for c in columns] 412 408 cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) 413 409 while 1: … … 429 425 if self._field.null: 430 426 date_query._where.append('%s.%s IS NOT NULL' % \ 431 427 (backend.quote_name(self.model._meta.db_table), backend.quote_name(self._field.column))) 432 select, sql, params = self._get_sql_clause( True)428 select, sql, params = self._get_sql_clause() 433 429 sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1 %s' % \ 434 430 (backend.get_date_trunc_sql(self._kind, '%s.%s' % (backend.quote_name(self.model._meta.db_table), 435 431 backend.quote_name(self._field.column))), sql, self._order) … … 762 758 params.extend(field.get_db_prep_lookup(clause, value)) 763 759 764 760 return tables, joins, where, params 761 762 def compare_models(x, y): 763 "Comparator for Models that puts models in an order where dependencies are easily resolved." 764 for field in x._meta.fields: 765 if field.rel and not field.null and field.rel.to == y: 766 return -1 767 for field in y._meta.fields: 768 if field.rel and not field.null and field.rel.to == x: 769 return 1 770 return 0 771 772 def delete_objects(seen_objs): 773 "Iterate through a list of seen classes, and remove any instances that are referred to" 774 seen_classes = set(seen_objs.keys()) 775 ordered_classes = list(seen_classes) 776 ordered_classes.sort(compare_models) 777 778 cursor = connection.cursor() 779 780 for cls in ordered_classes: 781 seen_objs[cls] = seen_objs[cls].items() 782 seen_objs[cls].sort() 783 784 # Pre notify all instances to be deleted 785 for pk_val, instance in seen_objs[cls]: 786 dispatcher.send(signal=signals.pre_delete, sender=cls, instance=instance) 787 788 pk_list = [pk for pk,instance in seen_objs[cls]] 789 for related in cls._meta.get_all_related_many_to_many_objects(): 790 cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ 791 (backend.quote_name(related.field.get_m2m_db_table(related.opts)), 792 backend.quote_name(cls._meta.object_name.lower() + '_id'), 793 ','.join('%s' for pk in pk_list)), 794 pk_list) 795 for f in cls._meta.many_to_many: 796 cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ 797 (backend.quote_name(f.get_m2m_db_table(cls._meta)), 798 backend.quote_name(cls._meta.object_name.lower() + '_id'), 799 ','.join(['%s' for pk in pk_list])), 800 pk_list) 801 for field in cls._meta.fields: 802 if field.rel and field.null and field.rel.to in seen_classes: 803 cursor.execute("UPDATE %s SET %s=NULL WHERE %s IN (%s)" % \ 804 (backend.quote_name(cls._meta.db_table), 805 backend.quote_name(field.column), 806 backend.quote_name(cls._meta.pk.column), 807 ','.join(['%s' for pk in pk_list])), 808 pk_list) 809 810 # Now delete the actual data 811 for cls in ordered_classes: 812 seen_objs[cls].reverse() 813 pk_list = [pk for pk,instance in seen_objs[cls]] 814 815 cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ 816 (backend.quote_name(cls._meta.db_table), 817 backend.quote_name(cls._meta.pk.column), 818 ','.join(['%s' for pk in pk_list])), 819 pk_list) 820 821 # Last cleanup; set NULLs where there once was a reference to the object, 822 # NULL the primary key of the found objects, and perform post-notification. 823 for pk_val, instance in seen_objs[cls]: 824 for field in cls._meta.fields: 825 if field.rel and field.null and field.rel.to in seen_classes: 826 setattr(instance, field.attname, None) 827 828 setattr(instance, cls._meta.pk.attname, None) 829 dispatcher.send(signal=signals.post_delete, sender=cls, instance=instance) 830 831 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. … … 37 39 >>> a.headline = 'Area woman programs in Python' 38 40 >>> a.save() 39 41 40 # Article.objects.all() returns all the articles in the database. Note that 41 # the article is represented by "<Article object>", because we haven't given 42 # the Article model a __repr__() method. 42 # Article.objects.all() returns all the articles in the database. 43 43 >>> Article.objects.all() 44 [ <Article object>]44 [Area woman programs in Python] 45 45 46 46 # Django provides a rich database lookup API. 47 47 >>> Article.objects.get(id__exact=1) 48 <Article object> 48 Area woman programs in Python 49 49 >>> Article.objects.get(headline__startswith='Area woman') 50 <Article object> 50 Area woman programs in Python 51 51 >>> Article.objects.get(pub_date__year=2005) 52 <Article object> 52 Area woman programs in Python 53 53 >>> Article.objects.get(pub_date__year=2005, pub_date__month=7) 54 <Article object> 54 Area woman programs in Python 55 55 >>> Article.objects.get(pub_date__year=2005, pub_date__month=7, pub_date__day=28) 56 <Article object> 56 Area woman programs in Python 57 57 58 58 # The "__exact" lookup type can be omitted, as a shortcut. 59 59 >>> Article.objects.get(id=1) 60 <Article object> 60 Area woman programs in Python 61 61 >>> Article.objects.get(headline='Area woman programs in Python') 62 <Article object> 62 Area woman programs in Python 63 63 64 64 >>> Article.objects.filter(pub_date__year=2005) 65 [ <Article object>]65 [Area woman programs in Python] 66 66 >>> Article.objects.filter(pub_date__year=2004) 67 67 [] 68 68 >>> Article.objects.filter(pub_date__year=2005, pub_date__month=7) 69 [ <Article object>]69 [Area woman programs in Python] 70 70 71 71 # Django raises an Article.DoesNotExist exception for get() if the parameters 72 72 # don't match any object. … … 84 84 # shortcut for primary-key exact lookups. 85 85 # The following is identical to articles.get(id=1). 86 86 >>> Article.objects.get(pk=1) 87 <Article object> 87 Area woman programs in Python 88 88 89 89 # Model instances of the same type and same ID are considered equal. 90 90 >>> a = Article.objects.get(pk=1) … … 234 234 235 235 # You can get items using index and slice notation. 236 236 >>> Article.objects.all()[0] 237 <Article object> 237 Area woman programs in Python 238 238 >>> Article.objects.all()[1:3] 239 [ <Article object>, <Article object>]239 [Second article, Third article] 240 240 >>> s3 = Article.objects.filter(id__exact=3) 241 241 >>> (s1 | s2 | s3)[::2] 242 [ <Article object>, <Article object>]242 [Area woman programs in Python, Third article] 243 243 244 244 # An Article instance doesn't have access to the "objects" attribute. 245 245 # That's only available on the class. … … 254 254 AttributeError: Manager isn't accessible via Article instances 255 255 256 256 # 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 257 >>> Article.objects.all() 258 [Area woman programs in Python, Second article, Third article, Fourth article, Article 6, Default headline, Article 7, Updated article 8] 259 >>> Article.objects.filter(id__lte=4).delete() 260 >>> Article.objects.all() 261 [Article 6, Default headline, Article 7, Updated article 8] 262 262 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 263 """ 273 264 274 265 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 >>> q = Article.objects.filter(headline__startswith='Django') 183 >>> print q 184 [Django lets you build Web apps easily] 185 >>> q.delete() 186 187 # After the delete, the QuerySet cache needs to be cleared, and the referenced objects should be gone 188 >>> print q 189 [] 190 >>> p1.article_set.all() 191 [NASA uses Python] 192 193 167 194 """ -
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 """