Code

Ticket #17332: 17332.diff

File 17332.diff, 14.4 KB (added by akaariai, 2 years ago)

POC patch

Line 
1diff --git a/django/db/models/base.py b/django/db/models/base.py
2index ebd67be..116a086 100644
3--- a/django/db/models/base.py
4+++ b/django/db/models/base.py
5@@ -2,6 +2,7 @@ import copy
6 import sys
7 from functools import update_wrapper
8 from itertools import izip
9+import warnings
10 
11 import django.db.models.manager     # Imported to register signal handler.
12 from django.conf import settings
13@@ -271,6 +272,20 @@ class ModelState(object):
14         # Necessary for correct validation of new instances of objects with explicit (non-auto) PKs.
15         # This impacts validation only; it has no effect on the actual save.
16         self.adding = True
17+        self.old_pk_values = {}
18+
19+    def update(self, obj, using, adding=False, pk_fields=None):
20+        if pk_fields is None:
21+            pk_fields = obj._meta.pk_fields
22+        self.db = using
23+        self.adding = adding
24+        for f in pk_fields:
25+            self.old_pk_values[f.attname] = f.to_immutable_value(obj)
26+
27+    def clear(self):
28+        self.db = None
29+        self.adding = True
30+        self.old_pk_values = {}
31 
32 class Model(object):
33     __metaclass__ = ModelBase
34@@ -429,6 +444,8 @@ class Model(object):
35         return getattr(self, meta.pk.attname)
36 
37     def _set_pk_val(self, value):
38+        if value is None:
39+            self._state.old_pk_values[self._meta.pk.attname] = None
40         return setattr(self, self._meta.pk.attname, value)
41 
42     pk = property(_get_pk_val, _set_pk_val)
43@@ -486,17 +503,16 @@ class Model(object):
44         if origin and not meta.auto_created:
45             signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using)
46 
47+        # Proxy-objects do not need saving. Skip to the parent objects.
48+        if meta.proxy:
49+            for parent, _ in meta.parents.items():
50+                self.save_base(cls=parent, origin=cls, using=using)
51+            return
52         # If we are in a raw save, save the object exactly as presented.
53         # That means that we don't try to be smart about saving attributes
54         # that might have come from the parent class - we just save the
55         # attributes we have been given to the class we have been given.
56-        # We also go through this process to defer the save of proxy objects
57-        # to their actual underlying model.
58-        if not raw or meta.proxy:
59-            if meta.proxy:
60-                org = cls
61-            else:
62-                org = None
63+        if not raw:
64             for parent, field in meta.parents.items():
65                 # At this point, parent's primary key field may be unknown
66                 # (for example, from administration form which doesn't fill
67@@ -504,67 +520,68 @@ class Model(object):
68                 if field and getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
69                     setattr(self, parent._meta.pk.attname, getattr(self, field.attname))
70 
71-                self.save_base(cls=parent, origin=org, using=using)
72+                self.save_base(cls=parent, origin=None, using=using)
73 
74                 if field:
75                     setattr(self, field.attname, self._get_pk_val(parent._meta))
76-            if meta.proxy:
77-                return
78-
79-        if not meta.proxy:
80-            non_pks = [f for f in meta.local_fields if not f.primary_key]
81-
82-            # First, try an UPDATE. If that doesn't update anything, do an INSERT.
83-            pk_val = self._get_pk_val(meta)
84-            pk_set = pk_val is not None
85-            record_exists = True
86-            manager = cls._base_manager
87-            if pk_set:
88-                # Determine whether a record with the primary key already exists.
89-                if (force_update or (not force_insert and
90-                        manager.using(using).filter(pk=pk_val).exists())):
91-                    # It does already exist, so do an UPDATE.
92-                    if force_update or non_pks:
93-                        values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks]
94-                        if values:
95-                            rows = manager.using(using).filter(pk=pk_val)._update(values)
96-                            if force_update and not rows:
97-                                raise DatabaseError("Forced update did not affect any rows.")
98-                else:
99-                    record_exists = False
100-            if not pk_set or not record_exists:
101-                if meta.order_with_respect_to:
102-                    # If this is a model with an order_with_respect_to
103-                    # autopopulate the _order field
104-                    field = meta.order_with_respect_to
105-                    order_value = manager.using(using).filter(**{field.name: getattr(self, field.attname)}).count()
106-                    self._order = order_value
107-
108-                fields = meta.local_fields
109-                if not pk_set:
110-                    if force_update:
111-                        raise ValueError("Cannot force an update in save() with no primary key.")
112-                    fields = [f for f in fields if not isinstance(f, AutoField)]
113 
114+        non_pks = [f for f in meta.local_fields if not f.primary_key]
115+        pk_val = self._get_pk_val(meta)
116+        pk_set = pk_val is not None
117+        record_exists = True
118+        manager = cls._base_manager
119+
120+        if pk_set and not self._state.adding:
121+            old_pk_val = self._state.old_pk_values[meta.pk.attname]
122+            if old_pk_val is not None and pk_val != old_pk_val:
123+                warnings.warn("Saving an object with changed primary key is deprecated. "
124+                              "You can use obj.pk = None; obj.pk = new_val to bypass this check.",
125+                              PendingDeprecationWarning)
126+
127+        # First, try an UPDATE. If that doesn't update anything, do an INSERT.
128+        if pk_set:
129+            # Determine whether a record with the primary key already exists.
130+            if (force_update or (not force_insert and
131+                    manager.using(using).filter(pk=pk_val).exists())):
132+                # It does already exist, so do an UPDATE.
133+                if force_update or non_pks:
134+                    values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks]
135+                    if values:
136+                        rows = manager.using(using).filter(pk=pk_val)._update(values)
137+                        if force_update and not rows:
138+                            raise DatabaseError("Forced update did not affect any rows.")
139+            else:
140                 record_exists = False
141-
142-                update_pk = bool(meta.has_auto_field and not pk_set)
143-                result = manager._insert([self], fields=fields, return_id=update_pk, using=using, raw=raw)
144-
145-                if update_pk:
146-                    setattr(self, meta.pk.attname, result)
147+        if not pk_set or not record_exists:
148+            if meta.order_with_respect_to:
149+                # If this is a model with an order_with_respect_to
150+                # autopopulate the _order field
151+                field = meta.order_with_respect_to
152+                order_value = manager.using(using).filter(**{field.name: getattr(self, field.attname)}).count()
153+                self._order = order_value
154+
155+            fields = meta.local_fields
156+            if not pk_set:
157+                if force_update:
158+                    raise ValueError("Cannot force an update in save() with no primary key.")
159+                fields = [f for f in fields if not isinstance(f, AutoField)]
160+
161+            record_exists = False
162+
163+            update_pk = bool(meta.has_auto_field and not pk_set)
164+            result = manager._insert([self], fields=fields, return_id=update_pk, using=using, raw=raw)
165+
166+            if update_pk:
167+                setattr(self, meta.pk.attname, result)
168+
169+        if origin:
170+            # We are at the topmost object - finalize the save
171             transaction.commit_unless_managed(using=using)
172-
173-        # Store the database on which the object was saved
174-        self._state.db = using
175-        # Once saved, this is no longer a to-be-added instance.
176-        self._state.adding = False
177-
178-        # Signal that the save is complete
179-        if origin and not meta.auto_created:
180-            signals.post_save.send(sender=origin, instance=self,
181-                created=(not record_exists), raw=raw, using=using)
182-
183+            self._state.update(self, using)
184+            if not meta.auto_created:
185+                # Signal that the save is complete
186+                signals.post_save.send(sender=origin, instance=self,
187+                    created=(not record_exists), raw=raw, using=using)
188 
189     save_base.alters_data = True
190 
191@@ -575,6 +592,7 @@ class Model(object):
192         collector = Collector(using=using)
193         collector.collect([self])
194         collector.delete()
195+        self._state.clear()
196 
197     delete.alters_data = True
198 
199diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
200index 04b13aa..1c6d796 100644
201--- a/django/db/models/fields/__init__.py
202+++ b/django/db/models/fields/__init__.py
203@@ -428,6 +428,14 @@ class Field(object):
204         """
205         return smart_unicode(self._get_val_from_obj(obj))
206 
207+    def to_immutable_value(self, obj):
208+        """
209+        Returns a value which is guaranteed to be immutable. This is used to
210+        track changes in field values. The returned value should be usable in
211+        database lookups.
212+        """
213+        return getattr(obj, self.attname)
214+
215     def bind(self, fieldmapping, original, bound_field_class):
216         return bound_field_class(self, fieldmapping, original)
217 
218diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py
219index d4b1743..211d05a 100644
220--- a/django/db/models/fields/files.py
221+++ b/django/db/models/fields/files.py
222@@ -249,6 +249,9 @@ class FileField(Field):
223             file.save(file.name, file, save=False)
224         return file
225 
226+    def to_immutable_value(self, obj):
227+        return getattr(obj, self.attname).name
228+
229     def contribute_to_class(self, cls, name):
230         super(FileField, self).contribute_to_class(cls, name)
231         setattr(cls, self.name, self.descriptor_class(self))
232diff --git a/django/db/models/options.py b/django/db/models/options.py
233index 0cd52a3..af33e62 100644
234--- a/django/db/models/options.py
235+++ b/django/db/models/options.py
236@@ -35,6 +35,7 @@ class Options(object):
237         self.db_tablespace = settings.DEFAULT_TABLESPACE
238         self.admin = None
239         self.meta = meta
240+        # The PK of the model's table
241         self.pk = None
242         self.has_auto_field, self.auto_field = False, None
243         self.abstract = False
244@@ -105,6 +106,7 @@ class Options(object):
245             self.db_table = "%s_%s" % (self.app_label, self.module_name)
246             self.db_table = truncate_name(self.db_table, connection.ops.max_name_length())
247 
248+
249     def _prepare(self, model):
250         if self.order_with_respect_to:
251             self.order_with_respect_to = self.get_field(self.order_with_respect_to)
252@@ -164,6 +166,7 @@ class Options(object):
253             if hasattr(self, '_field_cache'):
254                 del self._field_cache
255                 del self._field_name_cache
256+                del self._pk_field_cache
257 
258         if hasattr(self, '_name_map'):
259             del self._name_map
260@@ -219,6 +222,17 @@ class Options(object):
261         return self._field_name_cache
262     fields = property(_fields)
263 
264+    def _pk_fields(self):
265+        """
266+        All the PKs the model has, including parent tables.
267+        """
268+        try:
269+            self._field_name_cache
270+        except AttributeError:
271+            self._fill_fields_cache()
272+        return self._pk_field_cache
273+    pk_fields = property(_pk_fields)
274+
275     def get_fields_with_model(self):
276         """
277         Returns a sequence of (field, model) pairs for all fields. The "model"
278@@ -242,6 +256,7 @@ class Options(object):
279         cache.extend([(f, None) for f in self.local_fields])
280         self._field_cache = tuple(cache)
281         self._field_name_cache = [x for x, _ in cache]
282+        self._pk_field_cache = tuple(x for x, _ in cache if x.primary_key)
283 
284     def _many_to_many(self):
285         try:
286diff --git a/django/db/models/query.py b/django/db/models/query.py
287index 80cdefd..10a5527 100644
288--- a/django/db/models/query.py
289+++ b/django/db/models/query.py
290@@ -278,10 +278,11 @@ class QuerySet(object):
291                     init_list.append(field.attname)
292             model_cls = deferred_class_factory(self.model, skip)
293 
294-        # Cache db and model outside the loop
295+        # Cache some values outside the loop
296         db = self.db
297         model = self.model
298         compiler = self.query.get_compiler(using=db)
299+        pk_fields = model._meta.pk_fields
300         if fill_cache:
301             klass_info = get_klass_info(model, max_depth=max_depth,
302                                         requested=requested, only_load=only_load)
303@@ -297,10 +298,7 @@ class QuerySet(object):
304                     # Omit aggregates in object creation.
305                     obj = model(*row[index_start:aggregate_start])
306 
307-                # Store the source database of the object
308-                obj._state.db = db
309-                # This object came from the database; it's not being added.
310-                obj._state.adding = False
311+                obj._state.update(obj, db, pk_fields=pk_fields)
312 
313             if extra_select:
314                 for i, k in enumerate(extra_select):
315@@ -1316,7 +1314,7 @@ def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None,
316     return klass, field_names, field_count, related_fields, reverse_related_fields
317 
318 
319-def get_cached_row(row, index_start, using,  klass_info, offset=0):
320+def get_cached_row(row, index_start, using, klass_info, offset=0):
321     """
322     Helper function that recursively returns an object with the specified
323     related attributes already populated.
324@@ -1352,8 +1350,7 @@ def get_cached_row(row, index_start, using,  klass_info, offset=0):
325 
326     # If an object was retrieved, set the database state.
327     if obj:
328-        obj._state.db = using
329-        obj._state.adding = False
330+        obj._state.update(obj, using)
331 
332     # Instantiate related fields
333     index_end = index_start + field_count + offset
334@@ -1486,9 +1483,7 @@ class RawQuerySet(object):
335                 for column, pos in annotation_fields:
336                     setattr(instance, column, values[pos])
337 
338-            instance._state.db = db
339-            instance._state.adding = False
340-
341+            instance._state.update(instance, db)
342             yield instance
343 
344     def __repr__(self):